From 660258ed843fe4858b667e0e281f01d6d4d0b8b4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Oct 2022 10:34:17 +0200 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1eee05b106..4dc2c04594 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.0.0-SNAPSHOT + 3.0.0-GH-2348-SNAPSHOT Spring Data Redis Spring Data module for Redis From db74402b8c8ec1076ab43061a4787d8a51cabf53 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Oct 2022 10:33:58 +0200 Subject: [PATCH 2/2] Add observability adapter for Lettuce. We now provide MicrometerTracingAdapter to connect Lettuce to Micrometer Tracing. Closes #2348 --- pom.xml | 97 ++++- src/main/asciidoc/index.adoc | 1 + .../asciidoc/reference/observability.adoc | 38 ++ .../DefaultLettuceObservationConvention.java | 82 +++++ .../LettuceObservationContext.java | 72 ++++ .../LettuceObservationConvention.java | 34 ++ .../MicrometerTracingAdapter.java | 330 ++++++++++++++++++ .../observability/RedisObservation.java | 142 ++++++++ .../observability/SocketAddressEndpoint.java | 37 ++ .../lettuce/observability/package-info.java | 6 + .../observation/ReactiveIntegrationTests.java | 81 +++++ .../SynchronousIntegrationTests.java | 73 ++++ .../lettuce/observation/TestConfig.java | 63 ++++ 13 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 src/main/asciidoc/reference/observability.adoc create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/observation/ReactiveIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/observation/SynchronousIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/observation/TestConfig.java diff --git a/pom.xml b/pom.xml index 4dc2c04594..3ffa57063e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -28,6 +29,11 @@ 1.01 4.1.79.Final spring.data.redis + + + ${project.basedir} + .* + ${project.basedir}/target/ @@ -157,6 +163,44 @@ true + + + + io.micrometer + micrometer-observation + true + + + + io.micrometer + micrometer-tracing + true + + + + io.micrometer + micrometer-test + test + + + com.github.tomakehurst + wiremock-jre8-standalone + + + + + + io.micrometer + micrometer-tracing-test + test + + + + io.micrometer + micrometer-tracing-integration-test + test + + @@ -285,6 +329,57 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + generate-metrics-metadata + generate-resources + + java + + + io.micrometer.docs.metrics.DocsFromSources + + + + generate-tracing-metadata + generate-resources + + java + + + io.micrometer.docs.spans.DocsFromSources + + + + + + + io.micrometer + micrometer-docs-generator-spans + ${micrometer-docs-generator} + jar + + + io.micrometer + micrometer-docs-generator-metrics + ${micrometer-docs-generator} + jar + + + + true + + ${micrometer-docs-generator.inputPath} + ${micrometer-docs-generator.inclusionPattern} + ${micrometer-docs-generator.outputPath} + + + + org.apache.maven.plugins maven-assembly-plugin diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index a4eed4a83f..2866332d9c 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -28,6 +28,7 @@ include::{spring-data-commons-docs}/dependencies.adoc[leveloffset=+1] include::reference/introduction.adoc[leveloffset=+1] include::reference/why-sdr.adoc[leveloffset=+1] include::reference/redis.adoc[leveloffset=+1] +include::reference/observability.adoc[leveloffset=+1] include::reference/reactive-redis.adoc[leveloffset=+1] include::reference/redis-cluster.adoc[leveloffset=+1] include::reference/redis-repositories.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/reference/observability.adoc b/src/main/asciidoc/reference/observability.adoc new file mode 100644 index 0000000000..d41ae86b1c --- /dev/null +++ b/src/main/asciidoc/reference/observability.adoc @@ -0,0 +1,38 @@ +[[redis.observability]] +== Observability + +Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. +Spring Data Redis ships with a Micrometer integration through the Lettuce driver to collect observations during Redis interaction. +Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Redis command. + +To enable the integration, apply the following configuration to `LettuceClientConfiguration`: + +[source,java] +---- +@Configuration +class TracingConfig { + + @Bean + public ClientResources clientResources(ObservationRegistry observationRegistry) { + + return ClientResources.builder() + .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) + .build(); + } + + @Bean + public LettuceConnectionFactory lettuceConnectionFactory(ClientResources clientResources) { + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .clientResources(clientResources).build(); + RedisConfiguration redisConfiguration = …; + return new LettuceConnectionFactory(redisConfiguration, clientConfig); + } +} +---- + +include::../../../../target/_conventions.adoc[] + +include::../../../../target/_metrics.adoc[] + +include::../../../../target/_spans.adoc[] diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java new file mode 100644 index 0000000000..5a05c0b5c5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/DefaultLettuceObservationConvention.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import java.net.InetSocketAddress; +import java.util.Locale; + +import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.HighCardinalityCommandKeyNames; +import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.LowCardinalityCommandKeyNames; + +import io.lettuce.core.protocol.RedisCommand; +import io.lettuce.core.tracing.Tracing.Endpoint; +import io.micrometer.common.KeyValues; + +/** + * Default {@link LettuceObservationConvention} implementation. + * + * @author Mark Paluch + * @since 3.0 + */ +record DefaultLettuceObservationConvention( + boolean includeCommandArgsInSpanTags) implements LettuceObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(LettuceObservationContext context) { + + Endpoint ep = context.getRequiredEndpoint(); + KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DATABASE_SYSTEM.withValue("redis"), // + LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().name()), // + LowCardinalityCommandKeyNames.REDIS_SERVER.withValue(ep.toString())); + + if (ep instanceof SocketAddressEndpoint endpoint) { + + if (endpoint.socketAddress()instanceof InetSocketAddress inet) { + keyValues = keyValues + .and(KeyValues.of(LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(inet.getHostString()), + LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + inet.getPort()))); + } else { + keyValues = keyValues + .and(KeyValues.of(LowCardinalityCommandKeyNames.NET_PEER_ADDR.withValue(endpoint.toString()))); + } + } + + return keyValues; + } + + @Override + public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) { + + RedisCommand command = context.getRequiredCommand(); + + if (includeCommandArgsInSpanTags) { + + if (command.getArgs() != null) { + return KeyValues.of(HighCardinalityCommandKeyNames.STATEMENT + .withValue(command.getType().name() + " " + command.getArgs().toCommandString())); + } + + return KeyValues.of(HighCardinalityCommandKeyNames.STATEMENT.withValue(command.getType().name())); + } + + return KeyValues.empty(); + } + + @Override + public String getContextualName(LettuceObservationContext context) { + return context.getRequiredCommand().getType().name().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java new file mode 100644 index 0000000000..527d55e630 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationContext.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import org.springframework.lang.Nullable; + +import io.lettuce.core.protocol.RedisCommand; +import io.lettuce.core.tracing.Tracing.Endpoint; +import io.micrometer.observation.Observation; +import io.micrometer.observation.transport.Kind; +import io.micrometer.observation.transport.SenderContext; + +/** + * Micrometer {@link Observation.Context} holding Lettuce contextual details. + * + * @author Mark Paluch + * @since 3.0 + */ +class LettuceObservationContext extends SenderContext { + + private volatile @Nullable RedisCommand command; + + private volatile @Nullable Endpoint endpoint; + + public LettuceObservationContext(String serviceName) { + super((carrier, key, value) -> {}, Kind.CLIENT); + setRemoteServiceName(serviceName); + } + + public RedisCommand getRequiredCommand() { + + RedisCommand local = command; + + if (local == null) { + throw new IllegalArgumentException("LettuceObservationContext is not associated with a Command"); + } + + return local; + } + + public void setCommand(RedisCommand command) { + this.command = command; + } + + public Endpoint getRequiredEndpoint() { + + Endpoint local = endpoint; + + if (local == null) { + throw new IllegalArgumentException("LettuceObservationContext is not associated with a Endpoint"); + } + + return local; + } + + public void setEndpoint(Endpoint endpoint) { + this.endpoint = endpoint; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java new file mode 100644 index 0000000000..5e1d85b320 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/LettuceObservationConvention.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for {@link LettuceObservationContext}. + * + * @author Mark Paluch + * @since 3.0 + */ +interface LettuceObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof LettuceObservationContext; + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java new file mode 100644 index 0000000000..67fcd73648 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/MicrometerTracingAdapter.java @@ -0,0 +1,330 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import java.net.SocketAddress; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.redis.connection.lettuce.observability.RedisObservation.HighCardinalityCommandKeyNames; +import org.springframework.lang.Nullable; + +import io.lettuce.core.protocol.CompleteableCommand; +import io.lettuce.core.protocol.RedisCommand; +import io.lettuce.core.tracing.TraceContext; +import io.lettuce.core.tracing.TraceContextProvider; +import io.lettuce.core.tracing.Tracer; +import io.lettuce.core.tracing.Tracer.Span; +import io.lettuce.core.tracing.TracerProvider; +import io.lettuce.core.tracing.Tracing; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import reactor.core.publisher.Mono; + +/** + * {@link Tracing} adapter using Micrometer's {@link Observation}. + * + * @author Mark Paluch + * @since 3.0 + */ +public class MicrometerTracingAdapter implements Tracing { + + private static final Log log = LogFactory.getLog(MicrometerTracingAdapter.class); + + private final ObservationRegistry observationRegistry; + private final String serviceName; + private final boolean includeCommandArgsInSpanTags; + + private final LettuceObservationConvention observationConvention; + + /** + * Create a new {@link MicrometerTracingAdapter} instance. + * + * @param observationRegistry must not be {@literal null}. + * @param serviceName service name to be used. + */ + public MicrometerTracingAdapter(ObservationRegistry observationRegistry, String serviceName) { + this(observationRegistry, serviceName, false); + } + + /** + * Create a new {@link MicrometerTracingAdapter} instance. + * + * @param observationRegistry must not be {@literal null}. + * @param serviceName service name to be used. + * @param includeCommandArgsInSpanTags whether to attach the full command into the trace. Use this flag with caution + * as sensitive arguments will be captured in the observation spans and metric tags. + */ + public MicrometerTracingAdapter(ObservationRegistry observationRegistry, String serviceName, + boolean includeCommandArgsInSpanTags) { + + this.observationRegistry = observationRegistry; + this.serviceName = serviceName; + this.observationConvention = new DefaultLettuceObservationConvention(includeCommandArgsInSpanTags); + this.includeCommandArgsInSpanTags = includeCommandArgsInSpanTags; + } + + @Override + public TracerProvider getTracerProvider() { + return () -> new MicrometerTracer(observationRegistry); + } + + @Override + public TraceContextProvider initialTraceContextProvider() { + return new MicrometerTraceContextProvider(observationRegistry); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean includeCommandArgsInSpanTags() { + return includeCommandArgsInSpanTags; + } + + @Override + public Endpoint createEndpoint(SocketAddress socketAddress) { + return new SocketAddressEndpoint(socketAddress); + } + + /** + * {@link Tracer} implementation based on Micrometer's {@link ObservationRegistry}. + */ + class MicrometerTracer extends Tracer { + + private final ObservationRegistry observationRegistry; + + public MicrometerTracer(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public Tracer.Span nextSpan() { + return this.postProcessSpan(createObservation()); + } + + @Override + public Tracer.Span nextSpan(TraceContext traceContext) { + + if (traceContext instanceof MicrometerTraceContext micrometerTraceContext) { + + return micrometerTraceContext.observation == null ? nextSpan() + : postProcessSpan(createObservation().parentObservation(micrometerTraceContext.observation())); + } + + return nextSpan(); + } + + private Observation createObservation() { + return RedisObservation.REDIS_COMMAND_OBSERVATION.observation(observationRegistry, + () -> new LettuceObservationContext(serviceName)); + } + + private Tracer.Span postProcessSpan(Observation observation) { + + return observation != null && !observation.isNoop() + ? new MicrometerSpan(observation.observationConvention(observationConvention)) + : NoOpSpan.INSTANCE; + } + } + + /** + * No-op {@link Span} implemementation. + */ + static class NoOpSpan extends Tracer.Span { + + static final NoOpSpan INSTANCE = new NoOpSpan(); + + public NoOpSpan() {} + + @Override + public Tracer.Span start(RedisCommand command) { + return this; + } + + @Override + public Tracer.Span name(String name) { + return this; + } + + @Override + public Tracer.Span annotate(String value) { + return this; + } + + @Override + public Tracer.Span tag(String key, String value) { + return this; + } + + @Override + public Tracer.Span error(Throwable throwable) { + return this; + } + + @Override + public Tracer.Span remoteEndpoint(Tracing.Endpoint endpoint) { + return this; + } + + @Override + public void finish() {} + } + + /** + * Micrometer {@link Observation}-based {@link Span} implementation. + */ + static class MicrometerSpan extends Tracer.Span { + + private final Observation observation; + + private @Nullable RedisCommand command; + + public MicrometerSpan(Observation observation) { + this.observation = observation; + } + + @Override + public Span start(RedisCommand command) { + + ((LettuceObservationContext) observation.getContext()).setCommand(command); + + this.command = command; + + if (log.isDebugEnabled()) { + log.debug(String.format("Starting Observation for Command %s", command)); + } + + if (command instanceof CompleteableCommand completeableCommand) { + + completeableCommand.onComplete((o, throwable) -> { + + if (command.getOutput() != null) { + + String error = command.getOutput().getError(); + if (error != null) { + observation.highCardinalityKeyValue(HighCardinalityCommandKeyNames.ERROR.withValue(error)); + } else if (throwable != null) { + error(throwable); + } + } + + finish(); + }); + } else { + throw new IllegalArgumentException("Command " + command + + " must implement CompleteableCommand to attach Span completion to command completion"); + } + + observation.start(); + return this; + } + + @Override + public Span name(String name) { + return this; + } + + @Override + public Span annotate(String annotation) { + return this; + } + + @Override + public Span tag(String key, String value) { + observation.highCardinalityKeyValue(key, value); + return this; + } + + @Override + public Span error(Throwable throwable) { + + if (log.isDebugEnabled()) { + log.debug(String.format("Attaching error to Observation for Command %s", command)); + } + + observation.error(throwable); + return this; + } + + @Override + public Span remoteEndpoint(Endpoint endpoint) { + + ((LettuceObservationContext) observation.getContext()).setEndpoint(endpoint); + return this; + } + + @Override + public void finish() { + + if (log.isDebugEnabled()) { + log.debug(String.format("Stopping Observation for Command %s", command)); + } + + observation.stop(); + } + } + + /** + * {@link TraceContextProvider} using {@link ObservationRegistry}. + */ + record MicrometerTraceContextProvider(ObservationRegistry registry) implements TraceContextProvider { + + @Override + public TraceContext getTraceContext() { + + Observation observation = registry.getCurrentObservation(); + + if (observation == null) { + return null; + } + + return new MicrometerTraceContext(observation); + } + + @Override + public Mono getTraceContextLater() { + + return Mono.deferContextual(Mono::justOrEmpty).filter((it) -> { + return it.hasKey(TraceContext.class) || it.hasKey(Observation.class) + || it.hasKey(ObservationThreadLocalAccessor.KEY); + }).map((it) -> { + + if (it.hasKey(Observation.class)) { + return new MicrometerTraceContext(it.get(Observation.class)); + } + + if (it.hasKey(TraceContext.class)) { + return it.get(TraceContext.class); + } + + return new MicrometerTraceContext(it.get(ObservationThreadLocalAccessor.KEY)); + }); + } + } + + /** + * {@link TraceContext} implementation using {@link Observation}. + * + * @param observation + */ + record MicrometerTraceContext(Observation observation) implements TraceContext { + + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java new file mode 100644 index 0000000000..871ca296bb --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/RedisObservation.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013-2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * A Redis-based {@link io.micrometer.observation.Observation}. + * + * @author Mark Paluch + * @since 3.0 + */ +enum RedisObservation implements ObservationDocumentation { + + /** + * Timer created around a Redis command execution. + */ + REDIS_COMMAND_OBSERVATION { + + @Override + public String getName() { + return "spring.data.redis"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityCommandKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityCommandKeyNames.values(); + } + }; + + /** + * Enums related to low cardinality key names for Redis commands. + */ + enum LowCardinalityCommandKeyNames implements KeyName { + + /** + * Database system. + */ + DATABASE_SYSTEM { + @Override + public String asString() { + return "db.system"; + } + }, + + /** + * Name of the database host. + */ + NET_PEER_NAME { + @Override + public String asString() { + return "net.peer.name"; + } + }, + + /** + * Logical remote port number. + */ + NET_PEER_PORT { + @Override + public String asString() { + return "net.peer.port"; + } + }, + + /** + * Redis peer address. + */ + NET_PEER_ADDR { + @Override + public String asString() { + return "net.sock.peer.addr"; + } + }, + + /** + * Redis endpoint. + */ + REDIS_SERVER { + @Override + public String asString() { + return "spring.data.redis.server"; + } + }, + + /** + * Redis command value. + */ + REDIS_COMMAND { + @Override + public String asString() { + return "db.operation"; + } + } + + } + + /** + * Enums related to high cardinality key names for Redis commands. + */ + enum HighCardinalityCommandKeyNames implements KeyName { + + /** + * Redis statement. + */ + STATEMENT { + @Override + public String asString() { + return "db.statement"; + } + }, + + /** + * Redis error response. + */ + ERROR { + @Override + public String asString() { + return "spring.data.redis.command.error"; + } + } + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java new file mode 100644 index 0000000000..af9c738f86 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/SocketAddressEndpoint.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observability; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import io.lettuce.core.tracing.Tracing.Endpoint; + +/** + * @author Mark Paluch + */ +record SocketAddressEndpoint(SocketAddress socketAddress) implements Endpoint { + + @Override + public String toString() { + + if (socketAddress instanceof InetSocketAddress inet) { + return inet.getHostString() + ":" + inet.getPort(); + } + + return socketAddress.toString(); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java new file mode 100644 index 0000000000..e3231ef4c3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/observability/package-info.java @@ -0,0 +1,6 @@ +/** + * Integration of Micrometer Tracing for Lettuce Observability. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.redis.connection.lettuce.observability; diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/observation/ReactiveIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/ReactiveIntegrationTests.java new file mode 100644 index 0000000000..20e694a073 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/ReactiveIntegrationTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observation; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import io.micrometer.tracing.test.SampleTestRunner; +import reactor.test.StepVerifier; +import reactor.util.context.Context; + +/** + * Collection of tests that log metrics and tracing using the reactive API. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestConfig.class) +public class ReactiveIntegrationTests extends SampleTestRunner { + + @Autowired LettuceConnectionFactory connectionFactory; + + ReactiveIntegrationTests() { + super(SampleRunnerConfig.builder().build()); + } + + @Override + protected MeterRegistry createMeterRegistry() { + return TestConfig.METER_REGISTRY; + } + + @Override + protected ObservationRegistry createObservationRegistry() { + return TestConfig.OBSERVATION_REGISTRY; + } + + @Override + public SampleTestRunnerConsumer yourCode() { + + return (tracer, meterRegistry) -> { + + Observation intermediate = Observation.start("intermediate", createObservationRegistry()); + + ReactiveRedisConnection connection = connectionFactory.getReactiveConnection(); + + connection.ping().contextWrite(Context.of(ObservationThreadLocalAccessor.KEY, intermediate)) + .as(StepVerifier::create).expectNext("PONG").verifyComplete(); + + intermediate.stop(); + + connection.close(); + + assertThat(tracer.getFinishedSpans()).isNotEmpty(); + System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); + }; + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/observation/SynchronousIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/SynchronousIntegrationTests.java new file mode 100644 index 0000000000..bfc1e0caaf --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/SynchronousIntegrationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observation; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.test.SampleTestRunner; + +/** + * Collection of tests that log metrics and tracing using the synchronous API. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestConfig.class) +public class SynchronousIntegrationTests extends SampleTestRunner { + + + @Autowired LettuceConnectionFactory connectionFactory; + + SynchronousIntegrationTests() { + super(SampleRunnerConfig.builder().build()); + } + + @Override + protected MeterRegistry createMeterRegistry() { + return TestConfig.METER_REGISTRY; + } + + @Override + protected ObservationRegistry createObservationRegistry() { + return TestConfig.OBSERVATION_REGISTRY; + } + + @Override + public SampleTestRunnerConsumer yourCode() { + + return (tracer, meterRegistry) -> { + + RedisConnection connection = connectionFactory.getConnection(); + connection.ping(); + + connection.close(); + + assertThat(tracer.getFinishedSpans()).isNotEmpty(); + System.out.println(((SimpleMeterRegistry) meterRegistry).getMetersAsString()); + }; + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/observation/TestConfig.java b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/TestConfig.java new file mode 100644 index 0000000000..7d8d539d8a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/observation/TestConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 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 org.springframework.data.redis.connection.lettuce.observation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.observability.MicrometerTracingAdapter; + +import io.lettuce.core.resource.ClientResources; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; + +/** + * @author Mark Paluch + */ +@Configuration +class TestConfig { + + static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry(); + static final ObservationRegistry OBSERVATION_REGISTRY = ObservationRegistry.create(); + + static { + OBSERVATION_REGISTRY.observationConfig().observationHandler(new DefaultMeterObservationHandler(METER_REGISTRY)); + } + + @Bean(destroyMethod = "shutdown") + ClientResources clientResources(ObservationRegistry observationRegistry) { + return ClientResources.builder().tracing(new MicrometerTracingAdapter(observationRegistry, "Redis", true)) + .build(); + } + + @Bean + LettuceConnectionFactory connectionFactory(ClientResources clientResources) { + + LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() + .clientResources(clientResources).build(); + + return new LettuceConnectionFactory(SettingsUtils.standaloneConfiguration(), clientConfiguration); + } + + @Bean + ObservationRegistry registry() { + return OBSERVATION_REGISTRY; + } +}