Added TX support

fixes gh-1941
This commit is contained in:
Marcin Grzejszczak
2021-05-07 16:33:37 +02:00
parent 4fef057e96
commit 507e918c39
22 changed files with 939 additions and 43 deletions

View File

@@ -599,3 +599,11 @@ This feature is available for all tracer implementations.
If you have Spring Batch running on the classpath, we wrap the `StepBuilderFactory` and the `JobBuilderFactory` to propagate the tracing context.
In order to disable this instrumentation set `spring.sleuth.batch.enabled` to `false`.
[[sleuth-tx-integration]]
== Spring Tx
This feature is available for all tracer implementations.
If you have Spring Tx on the classpath we will instrument the `PlatformTransactionManager` and the `ReactiveTransactionManager` to create a span whenever a new transaction is created.
In order to disable this instrumentation set `spring.sleuth.tx.enabled` to `false`.

View File

@@ -30,7 +30,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Registers beans related to Spring Cloud Task scheduling.
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} that registers instrumentation for Spring Cloud Task.
*
* @author Marcin Grzejszczak
* @since 3.1.0

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.instrument.tx;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.sleuth.instrument.tx.TracePlatformTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Post processor that wraps a {@link PlatformTransactionManager}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class TracePlatformTransactionManagerBeanPostProcessor implements BeanPostProcessor {
private final BeanFactory beanFactory;
public TracePlatformTransactionManagerBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PlatformTransactionManager && !(bean instanceof TracePlatformTransactionManager)) {
return new TracePlatformTransactionManager((PlatformTransactionManager) bean, this.beanFactory);
}
return bean;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.instrument.tx;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.sleuth.instrument.tx.TraceReactiveTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
/**
* Post processor that wraps a {@link ReactiveTransactionManager}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class TraceReactiveTransactionManagerBeanPostProcessor implements BeanPostProcessor {
private final BeanFactory beanFactory;
public TraceReactiveTransactionManagerBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ReactiveTransactionManager && !(bean instanceof TraceReactiveTransactionManager)) {
return new TraceReactiveTransactionManager((ReactiveTransactionManager) bean, this.beanFactory);
}
return bean;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.instrument.tx;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} that registers instrumentation for Spring TX.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.tx.enabled", matchIfMissing = true)
@ConditionalOnBean(Tracer.class)
@AutoConfigureAfter(BraveAutoConfiguration.class)
public class TraceTxAutoConfiguration {
@Bean
@ConditionalOnClass(name = "org.springframework.transaction.PlatformTransactionManager")
static TracePlatformTransactionManagerBeanPostProcessor tracePlatformTransactionManagerBeanPostProcessor(
BeanFactory beanFactory) {
return new TracePlatformTransactionManagerBeanPostProcessor(beanFactory);
}
@Bean
@ConditionalOnClass(
name = { "org.springframework.transaction.ReactiveTransactionManager", "reactor.core.publisher.Mono" })
static TraceReactiveTransactionManagerBeanPostProcessor traceReactiveTransactionManagerBeanPostProcessor(
BeanFactory beanFactory) {
return new TraceReactiveTransactionManagerBeanPostProcessor(beanFactory);
}
}

View File

@@ -155,6 +155,12 @@
"description": "Enable Spring Cloud Config Server instrumentation.",
"defaultValue": true
},
{
"name": "spring.sleuth.tx.enabled",
"type": "java.lang.Boolean",
"description": "Enable Spring TX instrumentation.",
"defaultValue": true
},
{
"name": "spring.sleuth.batch.enabled",
"type": "java.lang.Boolean",

View File

@@ -21,6 +21,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringInte
org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.rsocket.TraceRSocketAutoConfiguration, \
org.springframework.cloud.sleuth.autoconfig.instrument.tx.TraceTxAutoConfiguration, \
org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.instrument.tx;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
class TraceTxAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.sleuth.noop.enabled=true")
.withConfiguration(AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceTxAutoConfiguration.class));
@Test
void should_register_bean_post_processors() {
this.contextRunner.run(context -> Assertions.assertThat(context)
.hasSingleBean(TracePlatformTransactionManagerBeanPostProcessor.class)
.hasSingleBean(TraceReactiveTransactionManagerBeanPostProcessor.class));
}
@Test
void should_not_register_bean_post_processor_when_tx_not_on_classpath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(PlatformTransactionManager.class))
.run(context -> Assertions.assertThat(context)
.doesNotHaveBean(TracePlatformTransactionManagerBeanPostProcessor.class));
}
@Test
void should_not_register_reactive_bean_post_processor_when_reactive_tx_not_on_classpath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ReactiveTransactionManager.class))
.run(context -> Assertions.assertThat(context)
.doesNotHaveBean(TraceReactiveTransactionManagerBeanPostProcessor.class));
}
@Test
void should_not_register_reactive_bean_post_processor_when_reactor_not_on_classpath() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Mono.class)).run(context -> Assertions
.assertThat(context).doesNotHaveBean(TraceReactiveTransactionManagerBeanPostProcessor.class));
}
}

View File

@@ -129,13 +129,14 @@ public class TraceAppDeployer implements AppDeployer {
@Override
public Mono<AppStatus> statusReactive(String id) {
return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status",
() -> this.delegate.statusReactive(id), span -> span.tag("deployer.app.id", id));
() -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id));
}
@Override
public Flux<AppStatus> statusesReactive(String... ids) {
return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses",
() -> this.delegate.statusesReactive(ids), span -> span.tag("deployer.app.ids", Arrays.toString(ids)));
() -> this.delegate.statusesReactive(ids),
(o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids)));
}
@Override

View File

@@ -16,8 +16,8 @@
package org.springframework.cloud.sleuth.instrument.reactor;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -333,18 +333,21 @@ public abstract class ReactorSleuth {
*/
public static <T> Mono<T> tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext,
@NonNull String childSpanName, @NonNull Supplier<Mono<T>> supplier,
@NonNull Consumer<Span> spanCustomizer) {
@NonNull BiConsumer<T, Span> spanCustomizer) {
return runMonoSupplierInScope(supplier, spanCustomizer).contextWrite(
context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName));
}
private static <T> Mono<T> runMonoSupplierInScope(Supplier<Mono<T>> supplier, Consumer<Span> spanCustomizer) {
private static <T> Mono<T> runMonoSupplierInScope(Supplier<Mono<T>> supplier, BiConsumer<T, Span> spanCustomizer) {
return Mono.deferContextual(contextView -> {
Span span = contextView.get(Span.class);
spanCustomizer.accept(span);
Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class);
// @formatter:off
return supplier.get()
.map(t -> {
spanCustomizer.accept(t, span);
return t;
})
// TODO: Fix me when this is resolved in Reactor
// .doOnSubscribe(__ -> scope.close())
.doOnError(span::error)
@@ -368,7 +371,7 @@ public abstract class ReactorSleuth {
*/
public static <T> Mono<T> tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext,
@NonNull String childSpanName, @NonNull Supplier<Mono<T>> supplier) {
return tracedMono(tracer, currentTraceContext, childSpanName, supplier, span -> {
return tracedMono(tracer, currentTraceContext, childSpanName, supplier, (o, span) -> {
});
}
@@ -382,7 +385,7 @@ public abstract class ReactorSleuth {
*/
public static <T> Mono<T> tracedMono(@NonNull Tracer tracer, @NonNull Span span,
@NonNull Supplier<Mono<T>> supplier) {
return runMonoSupplierInScope(supplier, span1 -> {
return runMonoSupplierInScope(supplier, (o, span1) -> {
}).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span));
}
@@ -399,7 +402,7 @@ public abstract class ReactorSleuth {
*/
public static <T> Flux<T> tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext,
@NonNull String childSpanName, @NonNull Supplier<Flux<T>> supplier,
@NonNull Consumer<Span> spanCustomizer) {
@NonNull BiConsumer<T, Span> spanCustomizer) {
return runFluxSupplierInScope(supplier, spanCustomizer).contextWrite(
context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName));
}
@@ -415,17 +418,20 @@ public abstract class ReactorSleuth {
*/
public static <T> Flux<T> tracedFlux(@NonNull Tracer tracer, @NonNull Span span,
@NonNull Supplier<Flux<T>> supplier) {
return runFluxSupplierInScope(supplier, span1 -> {
return runFluxSupplierInScope(supplier, (o, span1) -> {
}).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span));
}
private static <T> Flux<T> runFluxSupplierInScope(Supplier<Flux<T>> supplier, Consumer<Span> spanCustomizer) {
private static <T> Flux<T> runFluxSupplierInScope(Supplier<Flux<T>> supplier, BiConsumer<T, Span> spanCustomizer) {
return Flux.deferContextual(contextView -> {
Span span = contextView.get(Span.class);
spanCustomizer.accept(span);
Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class);
// @formatter:off
return supplier.get()
.map(t -> {
spanCustomizer.accept(t, span);
return t;
})
// TODO: Fix me when this is resolved in Reactor
// .doOnSubscribe(__ -> scope.close())
.doOnError(span::error)
@@ -449,24 +455,21 @@ public abstract class ReactorSleuth {
*/
public static <T> Flux<T> tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext,
@NonNull String childSpanName, @NonNull Supplier<Flux<T>> supplier) {
return tracedFlux(tracer, currentTraceContext, childSpanName, supplier, span -> {
return tracedFlux(tracer, currentTraceContext, childSpanName, supplier, (o, span) -> {
});
}
private static Span spanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext,
private static Span childSpanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext,
reactor.util.context.Context context, String childSpanName) {
TraceContext traceContext = context.getOrDefault(TraceContext.class, null);
Span span = null;
if (traceContext == null) {
span = context.getOrDefault(Span.class, null);
}
Span span = context.getOrDefault(Span.class, null);
if (traceContext == null && span == null) {
span = tracer.nextSpan();
if (log.isDebugEnabled()) {
log.debug("There was no previous span in reactor context, created a new one [" + span + "]");
}
}
else if (traceContext != null) {
else if (traceContext != null && span == null) {
// there was a previous span - we create a child one
try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) {
if (log.isDebugEnabled()) {
@@ -490,9 +493,19 @@ public abstract class ReactorSleuth {
return span.name(childSpanName).start();
}
private static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext,
/**
* Updates the Reactor context with tracing information. Creates a new span if there
* is no current span. Creates a child span if there was an entry in the context
* already.
* @param tracer tracer
* @param currentTraceContext current trace context
* @param context Reactor context
* @param childSpanName child span name when there is no span in context
* @return updated Reactor context
*/
public static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext,
reactor.util.context.Context context, String childSpanName) {
Span span = spanFromContext(tracer, currentTraceContext, context, childSpanName);
Span span = childSpanFromContext(tracer, currentTraceContext, context, childSpanName);
return putSpanInScope(tracer, context, span);
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.tx;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAndScope;
import org.springframework.cloud.sleuth.ThreadLocalSpan;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
/**
* A trace representation of a {@link PlatformTransactionManager}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class TracePlatformTransactionManager implements PlatformTransactionManager {
private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class);
private final PlatformTransactionManager delegate;
private final BeanFactory beanFactory;
private Tracer tracer;
volatile ThreadLocalSpan threadLocalSpan;
public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) {
this.delegate = delegate;
this.beanFactory = beanFactory;
}
@PostConstruct
void initialize() {
if (this.threadLocalSpan == null) {
this.threadLocalSpan = new ThreadLocalSpan(tracer());
}
}
@Override
public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
initialize();
SpanAndScope spanAndScope = this.threadLocalSpan.get();
Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan();
Span span = fallbackSpan();
try {
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
TransactionStatus status = this.delegate.getTransaction(definition);
span = taggedSpan(currentSpan, span, def, status);
return status;
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(
"Exception occurred while trying to get a transaction, will mark the span with error and report it");
}
span.error(e);
span.end();
throw e;
}
}
Span fallbackSpan() {
return tracer().nextSpan().name("tx").start();
}
private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) {
if (status.isNewTransaction() || currentSpan == null) {
log.info("Creating new span cause a new transaction is started");
TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass());
}
else {
span = currentSpan;
}
this.threadLocalSpan.set(span);
return span;
}
@Override
public void commit(TransactionStatus status) throws TransactionException {
SpanAndScope spanAndScope = this.threadLocalSpan.get();
if (spanAndScope == null) {
if (log.isDebugEnabled()) {
log.debug("No span and scope found - this shouldn't happen, sth is wrong");
}
this.delegate.commit(status);
return;
}
Exception ex = null;
Span span = spanAndScope.getSpan();
try {
if (log.isDebugEnabled()) {
log.debug("Wrapping commit");
}
this.delegate.commit(status);
}
catch (Exception e) {
ex = e;
span.error(e);
throw e;
}
finally {
span.event("tx commit");
span.end();
if (ex == null) {
if (log.isDebugEnabled()) {
log.debug("No exception was found - will clear thread local span");
}
this.threadLocalSpan.remove();
}
}
}
@Override
public void rollback(TransactionStatus status) throws TransactionException {
SpanAndScope spanAndScope = this.threadLocalSpan.get();
if (spanAndScope == null) {
if (log.isDebugEnabled()) {
log.debug("No span and scope found - this shouldn't happen, sth is wrong");
}
this.delegate.rollback(status);
return;
}
Span span = spanAndScope.getSpan();
try {
if (log.isDebugEnabled()) {
log.debug("Wrapping rollback");
}
this.delegate.rollback(status);
}
catch (Exception e) {
span.error(e);
throw e;
}
finally {
span.event("tx rollback");
span.end();
this.threadLocalSpan.remove();
}
}
private Tracer tracer() {
if (this.tracer == null) {
this.tracer = this.beanFactory.getBean(Tracer.class);
}
return this.tracer;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.tx;
import org.springframework.cloud.sleuth.Span;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
final class TracePlatformTransactionManagerTags {
private TracePlatformTransactionManagerTags() {
throw new IllegalStateException("Can't instantiate a utility class");
}
static void tag(Span span, TransactionDefinition def, Class transactionManagerClass) {
span.tag("tx.transaction-manager", ClassUtils.getQualifiedName(transactionManagerClass));
span.tag("tx.read-only", String.valueOf(def.isReadOnly()));
span.tag("tx.propagation-level", propagationLevel(def));
span.tag("tx.isolation-level", isolationLevel(def));
if (def.getTimeout() > 0) {
span.tag("tx.timeout", String.valueOf(def.getTimeout()));
}
if (StringUtils.hasText(def.getName())) {
span.tag("tx.name", def.getName());
}
}
private static String propagationLevel(TransactionDefinition def) {
switch (def.getPropagationBehavior()) {
case 0:
return "PROPAGATION_REQUIRED";
case 1:
return "PROPAGATION_SUPPORTS";
case 2:
return "PROPAGATION_MANDATORY";
case 3:
return "PROPAGATION_REQUIRES_NEW";
case 4:
return "PROPAGATION_NOT_SUPPORTED";
case 5:
return "PROPAGATION_NEVER";
case 6:
return "PROPAGATION_NESTED";
default:
return String.valueOf(def.getPropagationBehavior());
}
}
private static String isolationLevel(TransactionDefinition def) {
switch (def.getIsolationLevel()) {
case -1:
return "ISOLATION_DEFAULT";
case 1:
return "ISOLATION_READ_UNCOMMITTED";
case 2:
return "ISOLATION_READ_COMMITTED";
case 4:
return "ISOLATION_REPEATABLE_READ";
case 8:
return "ISOLATION_SERIALIZABLE";
default:
return String.valueOf(def.getIsolationLevel());
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.tx;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAndScope;
import org.springframework.cloud.sleuth.TraceContext;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.transaction.ReactiveTransaction;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
/**
* A trace representation of a {@link ReactiveTransactionManager}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class TraceReactiveTransactionManager implements ReactiveTransactionManager {
private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class);
private final ReactiveTransactionManager delegate;
private final BeanFactory beanFactory;
private Tracer tracer;
private CurrentTraceContext currentTraceContext;
public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) {
this.delegate = delegate;
this.beanFactory = beanFactory;
}
private Tracer tracer() {
if (this.tracer == null) {
this.tracer = this.beanFactory.getBean(Tracer.class);
}
return this.tracer;
}
private CurrentTraceContext currentTraceContext() {
if (this.currentTraceContext == null) {
this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class);
}
return this.currentTraceContext;
}
@Override
public Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition)
throws TransactionException {
return Mono.deferContextual(contextView -> {
return this.delegate.getReactiveTransaction(definition).map(tx -> {
Span span = span(contextView);
if (tx.isNewTransaction() || span == null) {
if (span == null) {
span = tracer().nextSpan().name("tx").start();
}
else {
span = tracer().nextSpan(span).name("tx").start();
}
TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass());
}
Tracer.SpanInScope withSpan = tracer().withSpan(span);
SpanAndScope spanAndScope = new SpanAndScope(span, withSpan);
return new TraceReactiveTransaction(tx, spanAndScope);
});
});
}
private Span span(reactor.util.context.ContextView contextView) {
Span span = contextView.getOrDefault(Span.class, null);
if (span == null) {
TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null);
if (traceContext == null) {
Span currentSpan = tracer().currentSpan();
if (log.isDebugEnabled()) {
log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan
+ "]");
}
span = currentSpan;
}
else {
span = spanFromContext(traceContext);
}
}
return span;
}
private Span spanFromContext(TraceContext traceContext) {
try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) {
return tracer().currentSpan();
}
}
@Override
public Mono<Void> commit(ReactiveTransaction transaction) throws TransactionException {
if (!(transaction instanceof TraceReactiveTransaction)) {
return this.delegate.commit(transaction);
}
TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction;
SpanAndScope spanAndScope = reactiveTransaction.spanAndScope;
Span span = spanAndScope.getSpan();
Tracer.SpanInScope scope = spanAndScope.getScope();
return this.delegate.commit(reactiveTransaction.delegate)
// TODO: Fix me when this is resolved in Reactor
// .doOnSubscribe(__ -> scope.close())
.doOnError(span::error).doOnSuccess(signalType -> {
span.end();
scope.close();
});
}
@Override
public Mono<Void> rollback(ReactiveTransaction transaction) throws TransactionException {
if (!(transaction instanceof TraceReactiveTransaction)) {
return this.delegate.rollback(transaction);
}
TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction;
SpanAndScope spanAndScope = reactiveTransaction.spanAndScope;
Span span = spanAndScope.getSpan();
Tracer.SpanInScope scope = spanAndScope.getScope();
return this.delegate.rollback(reactiveTransaction.delegate)
// TODO: Fix me when this is resolved in Reactor
// .doOnSubscribe(__ -> scope.close())
.doOnError(span::error).doFinally(signalType -> {
span.end();
if (scope != null) {
scope.close();
}
});
}
static class TraceReactiveTransaction implements ReactiveTransaction {
final ReactiveTransaction delegate;
final SpanAndScope spanAndScope;
TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) {
this.delegate = delegate;
this.spanAndScope = spanAndScope;
}
@Override
public boolean isNewTransaction() {
return this.delegate.isNewTransaction();
}
@Override
public void setRollbackOnly() {
this.delegate.setRollbackOnly();
}
@Override
public boolean isRollbackOnly() {
return this.delegate.isRollbackOnly();
}
@Override
public boolean isCompleted() {
return this.delegate.isCompleted();
}
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
* @author Marcin Grzejszczak
* @since 1.1.3
*/
// TODO: Move this to autoconfigure
public class OkHttpFeignClientBeanPostProcessor implements BeanPostProcessor {
private final BeanFactory beanFactory;

View File

@@ -27,6 +27,8 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packagesOf = ArchitectureTests.class, importOptions = ArchitectureTests.ProductionCode.class)
public class ArchitectureTests {
// TODO: Add "..org.springframework.beans.factory.config.." - BeanPostProcessors
// should end up in [autoconfig]
@ArchTest
public static final ArchRule should_not_contain_any_spring_configuration_reference_in_module = noClasses().should()
.dependOnClassesThat().resideInAnyPackage("..org.springframework.boot.context.properties..",

View File

@@ -34,6 +34,8 @@ import org.springframework.cloud.deployer.spi.app.AppScaleRequest;
import org.springframework.cloud.deployer.spi.app.AppStatus;
import org.springframework.cloud.deployer.spi.core.AppDefinition;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.sleuth.tracer.NoOpCurrentTraceContext;
import org.springframework.cloud.sleuth.tracer.SimpleTracer;
import org.springframework.core.env.Environment;
import org.springframework.core.io.PathResource;
import org.springframework.mock.env.MockEnvironment;

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.tx;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAndScope;
import org.springframework.cloud.sleuth.tracer.SimpleSpan;
import org.springframework.cloud.sleuth.tracer.SimpleTracer;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionTimedOutException;
import org.springframework.transaction.support.SimpleTransactionStatus;
import static org.assertj.core.api.BDDAssertions.then;
class TracePlatformTransactionManagerTests {
SimpleTracer tracer = new SimpleTracer();
PlatformTransactionManager delegate = BDDMockito.mock(PlatformTransactionManager.class);
@Test
void should_create_a_new_span_for_a_new_committed_transaction() {
// given
TracePlatformTransactionManager manager = manager();
setupTransactionStatusWithNewTransactionStatusEqualTo(true);
thenThreadLocalIsClear(manager);
// when
TransactionStatus transaction = manager.getTransaction(null);
// then
SimpleSpan span = thenATaggedSpanWasCreated(manager);
// when
manager.commit(transaction);
// then
thenOneSpanWasReported(manager, span);
}
@Test
void should_create_a_new_span_for_a_new_rolled_back_transaction() {
// given
TracePlatformTransactionManager manager = manager();
setupTransactionStatusWithNewTransactionStatusEqualTo(true);
thenThreadLocalIsClear(manager);
// when
TransactionStatus transaction = manager.getTransaction(null);
// then
SimpleSpan span = thenATaggedSpanWasCreated(manager);
// when
manager.rollback(transaction);
// then
thenOneSpanWasReported(manager, span);
}
@Test
void should_continue_a_span_for_a_the_same_transaction() {
// given
TracePlatformTransactionManager manager = managerWithManualFallback();
setupTransactionStatusWithNewTransactionStatusEqualTo(false);
SimpleSpan firstSpan = threadLocalSpan(manager);
// when
TransactionStatus transaction = manager.getTransaction(null);
// then
SpanAndScope spanAndScope = manager.threadLocalSpan.get();
then(spanAndScope).isNotNull();
then(spanAndScope.getSpan()).isSameAs(firstSpan);
// when
manager.commit(transaction);
// then
thenPreviouslyCreatedSpanWasFinished(firstSpan);
then(firstSpan).isSameAs(manager.threadLocalSpan.get().getSpan());
manager.threadLocalSpan.remove();
thenThreadLocalIsClear(manager);
}
private SimpleSpan threadLocalSpan(TracePlatformTransactionManager manager) {
SimpleSpan firstSpan = tracer.nextSpan().start();
manager.threadLocalSpan.set(firstSpan);
return firstSpan;
}
@Test
void should_report_a_fallback_span_when_exception_occurred_while_getting_transaction() {
TracePlatformTransactionManager manager = manager();
BDDMockito.given(this.delegate.getTransaction(BDDMockito.any()))
.willThrow(new TransactionTimedOutException("boom"));
thenThreadLocalIsClear(manager);
BDDAssertions.thenThrownBy(() -> manager.getTransaction(null)).isInstanceOf(TransactionTimedOutException.class);
SimpleSpan span = tracer.getOnlySpan();
then(span.throwable).isInstanceOf(TransactionTimedOutException.class);
}
@Test
void should_report_a_span_when_exception_occurred_while_committing_transaction() {
TracePlatformTransactionManager manager = manager();
BDDMockito.willThrow(new TransactionTimedOutException("boom")).given(this.delegate).commit(BDDMockito.any());
threadLocalSpan(manager);
BDDAssertions.thenThrownBy(() -> manager.commit(null)).isInstanceOf(TransactionTimedOutException.class);
SimpleSpan span = tracer.getOnlySpan();
then(span.throwable).isInstanceOf(TransactionTimedOutException.class);
manager.threadLocalSpan.remove();
thenThreadLocalIsClear(manager);
}
@Test
void should_report_a_span_when_exception_occurred_while_rolling_back_transaction() {
TracePlatformTransactionManager manager = manager();
BDDMockito.willThrow(new TransactionTimedOutException("boom")).given(this.delegate).rollback(BDDMockito.any());
threadLocalSpan(manager);
BDDAssertions.thenThrownBy(() -> manager.rollback(null)).isInstanceOf(TransactionTimedOutException.class);
SimpleSpan span = tracer.getOnlySpan();
then(span.throwable).isInstanceOf(TransactionTimedOutException.class);
manager.threadLocalSpan.remove();
thenThreadLocalIsClear(manager);
}
private void setupTransactionStatusWithNewTransactionStatusEqualTo(boolean transactionStatus) {
BDDMockito.given(this.delegate.getTransaction(BDDMockito.any()))
.willReturn(new SimpleTransactionStatus(transactionStatus));
}
private void thenThreadLocalIsClear(TracePlatformTransactionManager manager) {
then(manager.threadLocalSpan.get()).as("Thread local was cleared").isNull();
}
private void thenOneSpanWasReported(TracePlatformTransactionManager manager, SimpleSpan firstSpan) {
thenPreviouslyCreatedSpanWasFinished(firstSpan);
thenThreadLocalIsClear(manager);
}
private void thenPreviouslyCreatedSpanWasFinished(SimpleSpan firstSpan) {
then(firstSpan.ended).as("The previously created span was finished").isTrue();
then(tracer.getOnlySpan()).as("The previously created span was reported").isSameAs(firstSpan);
}
private TracePlatformTransactionManager manager() {
final TracePlatformTransactionManager manager = new TracePlatformTransactionManager(this.delegate,
beanFactory());
manager.initialize();
return manager;
}
private TracePlatformTransactionManager managerWithManualFallback() {
final TracePlatformTransactionManager manager = new TracePlatformTransactionManager(this.delegate,
beanFactory()) {
@Override
Span fallbackSpan() {
return new SimpleSpan().start();
}
};
manager.initialize();
return manager;
}
private SimpleSpan thenATaggedSpanWasCreated(TracePlatformTransactionManager manager) {
SpanAndScope spanAndScope = manager.threadLocalSpan.get();
then(spanAndScope).isNotNull();
SimpleSpan span = (SimpleSpan) spanAndScope.getSpan();
then(span.started).isTrue();
then(span.tags).isNotEmpty();
return span;
}
private BeanFactory beanFactory() {
StaticListableBeanFactory beanFactory = new StaticListableBeanFactory();
beanFactory.addBean("tracer", tracer);
return beanFactory;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.deployer;
package org.springframework.cloud.sleuth.tracer;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
@@ -29,7 +29,7 @@ import org.springframework.cloud.sleuth.TraceContext;
* @author Marcin Grzejszczak
* @since 3.0.0
*/
class NoOpCurrentTraceContext implements CurrentTraceContext {
public class NoOpCurrentTraceContext implements CurrentTraceContext {
@Override
public TraceContext context() {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.deployer;
package org.springframework.cloud.sleuth.tracer;
import org.springframework.cloud.sleuth.Tracer;
@@ -24,7 +24,7 @@ import org.springframework.cloud.sleuth.Tracer;
* @author Marcin Grzejszczak
* @since 3.0.0
*/
class NoOpSpanInScope implements Tracer.SpanInScope {
public class NoOpSpanInScope implements Tracer.SpanInScope {
@Override
public void close() {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.deployer;
package org.springframework.cloud.sleuth.tracer;
import org.springframework.cloud.sleuth.TraceContext;
@@ -24,7 +24,7 @@ import org.springframework.cloud.sleuth.TraceContext;
* @author Marcin Grzejszczak
* @since 3.0.0
*/
class NoOpTraceContext implements TraceContext {
public class NoOpTraceContext implements TraceContext {
@Override
public String traceId() {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.deployer;
package org.springframework.cloud.sleuth.tracer;
import java.util.HashMap;
import java.util.Map;
@@ -28,13 +28,15 @@ import org.springframework.cloud.sleuth.TraceContext;
* @author Marcin Grzejszczak
* @since 3.0.0
*/
class SimpleSpan implements Span {
public class SimpleSpan implements Span {
Map<String, String> tags = new HashMap<>();
public Map<String, String> tags = new HashMap<>();
boolean started;
public boolean started;
boolean ended;
public boolean ended;
public Throwable throwable;
@Override
public boolean isNoop() {
@@ -47,29 +49,30 @@ class SimpleSpan implements Span {
}
@Override
public Span start() {
public SimpleSpan start() {
this.started = true;
return this;
}
@Override
public Span name(String name) {
public SimpleSpan name(String name) {
return this;
}
@Override
public Span event(String value) {
public SimpleSpan event(String value) {
return this;
}
@Override
public Span tag(String key, String value) {
public SimpleSpan tag(String key, String value) {
this.tags.put(key, value);
return this;
}
@Override
public Span error(Throwable throwable) {
public SimpleSpan error(Throwable throwable) {
this.throwable = throwable;
return this;
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.deployer;
package org.springframework.cloud.sleuth.tracer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -36,16 +36,16 @@ import org.springframework.cloud.sleuth.Tracer;
* @author Marcin Grzejszczak
* @since 3.0.0
*/
class SimpleTracer implements Tracer {
public class SimpleTracer implements Tracer {
List<SimpleSpan> spans = new ArrayList<>();
public List<SimpleSpan> spans = new ArrayList<>();
@Override
public Span nextSpan(Span parent) {
return new SimpleSpan();
}
SimpleSpan getOnlySpan() {
public SimpleSpan getOnlySpan() {
BDDAssertions.then(this.spans).hasSize(1);
SimpleSpan span = this.spans.get(0);
BDDAssertions.then(span.started).as("Span must be started").isTrue();
@@ -69,7 +69,7 @@ class SimpleTracer implements Tracer {
}
@Override
public Span nextSpan() {
public SimpleSpan nextSpan() {
final SimpleSpan span = new SimpleSpan();
this.spans.add(span);
return span;