Remove MicrometerTracingAdapter in favor of Lettuce's Micrometer support.

Closes #3093
This commit is contained in:
Mark Paluch
2025-01-16 11:50:08 +01:00
parent 46c59b4d0d
commit a69a02e319
8 changed files with 5 additions and 832 deletions

View File

@@ -2,7 +2,7 @@
= 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.
Lettuce ships with a Micrometer integration 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`:
@@ -16,7 +16,7 @@ class ObservabilityConfiguration {
public ClientResources clientResources(ObservationRegistry observationRegistry) {
return ClientResources.builder()
.tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache"))
.tracing(new MicrometerTracing(observationRegistry, "my-redis-cache"))
.build();
}
@@ -31,77 +31,7 @@ class ObservabilityConfiguration {
}
----
See also https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] for further reference.
See also for further reference:
* https://redis.github.io/lettuce/advanced-usage/#micrometer[Lettuce Tracing]
* https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] .
[[observability-metrics]]
== Observability - Metrics
Below you can find a list of all metrics declared by this project.
[[observability-metrics-redis-command-observation]]
== Redis Command Observation
____
Timer created around a Redis command execution.
____
**Metric name** `spring.data.redis`. **Type** `timer` and **base unit** `seconds`.
Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`.
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`db.operation`|Redis command value.
|`db.redis.database_index`|Redis database index.
|`db.system`|Database system.
|`db.user`|Redis user.
|`net.peer.name`|Name of the database host.
|`net.peer.port`|Logical remote port number.
|`net.sock.peer.addr`|Mongo peer address.
|`net.sock.peer.port`|Mongo peer port.
|`net.transport`|Network transport.
|===
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`db.statement`|Redis statement.
|`spring.data.redis.command.error`|Redis error response.
|===
[[observability-spans]]
== Observability - Spans
Below you can find a list of all spans declared by this project.
[[observability-spans-redis-command-observation]]
== Redis Command Observation Span
> Timer created around a Redis command execution.
**Span name** `spring.data.redis`.
Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`.
.Tag Keys
|===
|Name | Description
|`db.operation`|Redis command value.
|`db.redis.database_index`|Redis database index.
|`db.statement`|Redis statement.
|`db.system`|Database system.
|`db.user`|Redis user.
|`net.peer.name`|Name of the database host.
|`net.peer.port`|Logical remote port number.
|`net.sock.peer.addr`|Mongo peer address.
|`net.sock.peer.port`|Mongo peer port.
|`net.transport`|Network transport.
|`spring.data.redis.command.error`|Redis error response.
|===

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2022-2025 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.lettuce.core.protocol.RedisCommand;
import io.lettuce.core.tracing.Tracing.Endpoint;
import io.micrometer.common.KeyValues;
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;
/**
* Default {@link LettuceObservationConvention} implementation.
*
* @author Mark Paluch
* @since 3.0
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
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().toString()));
if (ep instanceof SocketAddressEndpoint endpoint) {
if (endpoint.socketAddress() instanceof InetSocketAddress inet) {
keyValues = keyValues
.and(KeyValues.of(LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(inet.getHostString()),
LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + inet.getPort()),
LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP")));
} else {
keyValues = keyValues
.and(KeyValues.of(LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(endpoint.toString()),
LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("Unix")));
}
}
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().toString() + " " + command.getArgs().toCommandString()));
}
}
return KeyValues.empty();
}
@Override
public String getContextualName(LettuceObservationContext context) {
return context.getRequiredCommand().getType().toString().toLowerCase(Locale.ROOT);
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2022-2025 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.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;
import org.springframework.lang.Nullable;
/**
* Micrometer {@link Observation.Context} holding Lettuce contextual details.
*
* @author Mark Paluch
* @since 3.0
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
public class LettuceObservationContext extends SenderContext<Object> {
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;
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2022-2025 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
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
interface LettuceObservationConvention extends ObservationConvention<LettuceObservationContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof LettuceObservationContext;
}
}

View File

@@ -1,340 +0,0 @@
/*
* Copyright 2022-2025 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.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;
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;
/**
* {@link Tracing} adapter using Micrometer's {@link Observation}. This adapter integrates with Micrometer to propagate
* observations into timers, distributed traces and any other registered handlers. Observations include a set of tags
* capturing Redis runtime information.
* <h3>Capturing full statements</h3> This adapter can capture full statements when enabling
* {@code includeCommandArgsInSpanTags}. You should carefully consider the impact of this setting as all command
* arguments will be captured in traces including these that may contain sensitive details.
*
* @author Mark Paluch
* @author Yanming Zhou
* @since 3.0
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
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}.
*/
public class MicrometerTracer extends Tracer {
private final ObservationRegistry observationRegistry;
public MicrometerTracer(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
@Override
public Tracer.Span nextSpan() {
return this.postProcessSpan(createObservation(null));
}
@Override
public Tracer.Span nextSpan(TraceContext traceContext) {
return postProcessSpan(createObservation(traceContext));
}
private Observation createObservation(@Nullable TraceContext parentContext) {
return RedisObservation.REDIS_COMMAND_OBSERVATION.observation(observationRegistry, () -> {
LettuceObservationContext context = new LettuceObservationContext(serviceName);
if (parentContext instanceof MicrometerTraceContext traceContext) {
context.setParentObservation(traceContext.observation());
}
return context;
});
}
private Tracer.Span postProcessSpan(Observation observation) {
return !observation.isNoop() ? new MicrometerSpan(observation.observationConvention(observationConvention))
: NoOpSpan.INSTANCE;
}
}
/**
* No-op {@link Span} implementation.
*/
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("Starting Observation for Command %s".formatted(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("Attaching error to Observation for Command %s".formatted(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("Stopping Observation for Command %s".formatted(command));
}
observation.stop();
}
}
/**
* {@link TraceContextProvider} using {@link ObservationRegistry}.
*/
record MicrometerTraceContextProvider(ObservationRegistry registry) implements TraceContextProvider {
@Override
@Nullable
public TraceContext getTraceContext() {
Observation observation = registry.getCurrentObservation();
if (observation == null) {
return null;
}
return new MicrometerTraceContext(observation);
}
@Override
public Mono<TraceContext> 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 {
}
}

View File

@@ -1,175 +0,0 @@
/*
* Copyright 2013-2025 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
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
public 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";
}
},
/**
* Network transport.
*/
NET_TRANSPORT {
@Override
public String asString() {
return "net.transport";
}
},
/**
* 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";
}
},
/**
* Mongo peer address.
*/
NET_SOCK_PEER_ADDR {
@Override
public String asString() {
return "net.sock.peer.addr";
}
},
/**
* Mongo peer port.
*/
NET_SOCK_PEER_PORT {
@Override
public String asString() {
return "net.sock.peer.port";
}
},
/**
* Redis user.
*/
DB_USER {
@Override
public String asString() {
return "db.user";
}
},
/**
* Redis database index.
*/
DB_INDEX {
@Override
public String asString() {
return "db.redis.database_index";
}
},
/**
* 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";
}
}
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2022-2025 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.lettuce.core.tracing.Tracing.Endpoint;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
/**
* @author Mark Paluch
* @deprecated since 3.4 for removal with the next major revision. Use Lettuce's Micrometer integration through
* {@link io.lettuce.core.tracing.MicrometerTracing}.
*/
@Deprecated(since = "3.4", forRemoval = true)
record SocketAddressEndpoint(SocketAddress socketAddress) implements Endpoint {
@Override
public String toString() {
if (socketAddress instanceof InetSocketAddress inet) {
return inet.getHostString() + ":" + inet.getPort();
}
return socketAddress.toString();
}
}

View File

@@ -1,6 +0,0 @@
/**
* Integration of Micrometer Tracing for Lettuce Observability.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.redis.connection.lettuce.observability;