Skip to content

Commit beafc3a

Browse files
shivangshahmarcingrzejszczak
authored andcommitted
Support rx java
RxJava Support fixes #235
1 parent 60cb5eb commit beafc3a

File tree

9 files changed

+382
-10
lines changed

9 files changed

+382
-10
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ _site/
1515
*.logtjmeter
1616
.checkstyle
1717
.DS_Store
18-
*.log
18+
*.log
19+
/spring-cloud-sleuth-core/nb-configuration.xml
20+
/spring-cloud-sleuth-core/nbactions.xml

docs/src/main/asciidoc/spring-cloud-sleuth.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,13 @@ In order to pass the tracing information you have to wrap the same logic in the
407407
include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/hystrix/TraceCommandTests.java[tags=trace_hystrix_command,indent=0]
408408
----
409409

410+
=== `RxJavaSchedulersHook` to add support for `RxJava` Schedulers
411+
412+
We're registering a custom https://github.com/ReactiveX/RxJava/wiki/Plugins#rxjavaschedulershook [`RxJavaSchedulersHook`]
413+
that wraps all `Action0` instances into their Sleuth representative -
414+
the `TraceAction`. The hook either starts or continues a span depending on the fact whether tracing was already going
415+
on before the Action was scheduled. To disable the custom RxJavaSchedulersHook set the `spring.sleuth.rxjava.schedulers.hook.enabled` to `false`.
416+
410417
=== HTTP integration
411418

412419
Features from this section can be disabled by providing the `spring.sleuth.web.enabled` property with value equal to `false`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.springframework.cloud.sleuth.instrument.rxjava;
2+
3+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
4+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7+
import org.springframework.cloud.sleuth.TraceKeys;
8+
import org.springframework.cloud.sleuth.Tracer;
9+
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import rx.plugins.RxJavaSchedulersHook;
13+
14+
/**
15+
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} that
16+
* enables support for RxJava via {@link RxJavaSchedulersHook}.
17+
*
18+
* @author Shivang Shah
19+
* @since 1.0.0
20+
*/
21+
@Configuration
22+
@AutoConfigureAfter(TraceAutoConfiguration.class)
23+
@ConditionalOnBean(Tracer.class)
24+
@ConditionalOnClass(RxJavaSchedulersHook.class)
25+
@ConditionalOnProperty(value = "spring.sleuth.rxjava.schedulers.hook.enabled", matchIfMissing = true)
26+
public class RxJavaAutoConfiguration {
27+
28+
@Bean
29+
SleuthRxJavaSchedulersHook sleuthRxJavaSchedulersHook(Tracer tracer, TraceKeys traceKeys) {
30+
return new SleuthRxJavaSchedulersHook(tracer, traceKeys);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package org.springframework.cloud.sleuth.instrument.rxjava;
2+
3+
import org.apache.commons.logging.Log;
4+
import org.apache.commons.logging.LogFactory;
5+
import org.springframework.cloud.sleuth.Span;
6+
import org.springframework.cloud.sleuth.TraceKeys;
7+
import org.springframework.cloud.sleuth.Tracer;
8+
import rx.functions.Action0;
9+
import rx.plugins.RxJavaErrorHandler;
10+
import rx.plugins.RxJavaObservableExecutionHook;
11+
import rx.plugins.RxJavaSchedulersHook;
12+
import rx.plugins.SleuthRxJavaPlugins;
13+
14+
/**
15+
* {@link RxJavaSchedulersHook} that wraps a {@link Action0} into its tracing
16+
* representation.
17+
*
18+
* @author Shivang Shah
19+
* @since 1.0.0
20+
*/
21+
class SleuthRxJavaSchedulersHook extends RxJavaSchedulersHook {
22+
23+
private static final Log log = LogFactory.getLog(SleuthRxJavaSchedulersHook.class);
24+
25+
private static final String RXJAVA_COMPONENT = "rxjava";
26+
private final Tracer tracer;
27+
private final TraceKeys traceKeys;
28+
private RxJavaSchedulersHook delegate;
29+
30+
SleuthRxJavaSchedulersHook(Tracer tracer, TraceKeys traceKeys) {
31+
this.tracer = tracer;
32+
this.traceKeys = traceKeys;
33+
try {
34+
this.delegate = SleuthRxJavaPlugins.getInstance().getSchedulersHook();
35+
if (this.delegate instanceof SleuthRxJavaSchedulersHook) {
36+
return;
37+
}
38+
RxJavaErrorHandler errorHandler = SleuthRxJavaPlugins.getInstance().getErrorHandler();
39+
RxJavaObservableExecutionHook observableExecutionHook
40+
= SleuthRxJavaPlugins.getInstance().getObservableExecutionHook();
41+
logCurrentStateOfRxJavaPlugins(errorHandler, observableExecutionHook);
42+
SleuthRxJavaPlugins.resetPlugins();
43+
SleuthRxJavaPlugins.getInstance().registerSchedulersHook(this);
44+
SleuthRxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
45+
SleuthRxJavaPlugins.getInstance().registerObservableExecutionHook(observableExecutionHook);
46+
} catch (Exception e) {
47+
log.error("Failed to register Sleuth RxJava SchedulersHook", e);
48+
}
49+
}
50+
51+
private void logCurrentStateOfRxJavaPlugins(RxJavaErrorHandler errorHandler,
52+
RxJavaObservableExecutionHook observableExecutionHook) {
53+
log.debug("Current RxJava plugins configuration is ["
54+
+ "schedulersHook [" + this.delegate + "],"
55+
+ "errorHandler [" + errorHandler + "],"
56+
+ "observableExecutionHook [" + observableExecutionHook + "],"
57+
+ "]");
58+
log.debug("Registering Sleuth RxJava Schedulers Hook.");
59+
}
60+
61+
@Override
62+
public Action0 onSchedule(Action0 action) {
63+
if (action instanceof TraceAction) {
64+
return action;
65+
}
66+
Action0 wrappedAction = this.delegate != null
67+
? this.delegate.onSchedule(action) : action;
68+
if (wrappedAction instanceof TraceAction) {
69+
return action;
70+
}
71+
return super.onSchedule(new TraceAction(this.tracer, this.traceKeys, wrappedAction));
72+
}
73+
74+
static class TraceAction implements Action0 {
75+
76+
private final Action0 actual;
77+
private Tracer tracer;
78+
private TraceKeys traceKeys;
79+
private Span parent;
80+
81+
public TraceAction(Tracer tracer, TraceKeys traceKeys, Action0 actual) {
82+
this.tracer = tracer;
83+
this.traceKeys = traceKeys;
84+
this.parent = tracer.getCurrentSpan();
85+
this.actual = actual;
86+
}
87+
88+
@Override
89+
public void call() {
90+
Span span = this.parent;
91+
boolean created = false;
92+
if (span != null) {
93+
span = this.tracer.continueSpan(span);
94+
} else {
95+
span = this.tracer.createSpan(RXJAVA_COMPONENT);
96+
this.tracer.addTag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, RXJAVA_COMPONENT);
97+
this.tracer.addTag(this.traceKeys.getAsync().getPrefix()
98+
+ this.traceKeys.getAsync().getThreadNameKey(), Thread.currentThread().getName());
99+
created = true;
100+
}
101+
try {
102+
this.actual.call();
103+
} finally {
104+
if (created) {
105+
this.tracer.close(span);
106+
} else {
107+
this.tracer.detach(span);
108+
}
109+
}
110+
}
111+
}
112+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package rx.plugins;
2+
3+
/**
4+
* {@link RxJavaPlugins} helper class to access the package scope method
5+
* of {@link RxJavaPlugins#reset()}. Will disappear once this gets closed
6+
* https://github.com/ReactiveX/RxJava/issues/2297
7+
*
8+
* @author Shivang Shah
9+
* @since 1.0.0
10+
*/
11+
public class SleuthRxJavaPlugins extends RxJavaPlugins {
12+
13+
SleuthRxJavaPlugins() {
14+
super();
15+
}
16+
17+
public static void resetPlugins() {
18+
getInstance().reset();
19+
}
20+
}

spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration,\
1414
org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration,\
1515
org.springframework.cloud.sleuth.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\
1616
org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\
17-
org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration
17+
org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration,\
18+
org.springframework.cloud.sleuth.instrument.rxjava.RxJavaAutoConfiguration
1819

1920
# Environment Post Processor
2021
org.springframework.boot.env.EnvironmentPostProcessor=\

spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/LogTest.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
package org.springframework.cloud.sleuth;
1818

19-
import com.fasterxml.jackson.databind.JsonMappingException;
20-
import com.fasterxml.jackson.databind.ObjectMapper;
2119
import java.io.IOException;
20+
2221
import org.junit.Rule;
2322
import org.junit.Test;
2423
import org.junit.rules.ExpectedException;
2524

25+
import com.fasterxml.jackson.databind.JsonMappingException;
26+
import com.fasterxml.jackson.databind.ObjectMapper;
27+
2628
import static org.assertj.core.api.BDDAssertions.then;
2729

2830
/**
@@ -36,24 +38,24 @@ public class LogTest {
3638
ObjectMapper objectMapper = new ObjectMapper();
3739

3840
@Test public void ctor_missing_event() throws IOException {
39-
thrown.expect(NullPointerException.class);
40-
thrown.expectMessage("event");
41+
this.thrown.expect(NullPointerException.class);
42+
this.thrown.expectMessage("event");
4143

4244
new Log(1234L, null);
4345
}
4446

4547
@Test public void serialization_round_trip() throws IOException {
4648
Log log = new Log(1234L, "cs");
4749

48-
String serialized = objectMapper.writeValueAsString(log);
49-
Log deserialized = objectMapper.readValue(serialized, Log.class);
50+
String serialized = this.objectMapper.writeValueAsString(log);
51+
Log deserialized = this.objectMapper.readValue(serialized, Log.class);
5052

5153
then(deserialized).isEqualTo(log);
5254
}
5355

5456
@Test public void deserialize_missing_event() throws IOException {
55-
thrown.expect(JsonMappingException.class);
57+
this.thrown.expect(JsonMappingException.class);
5658

57-
objectMapper.readValue("{\"timestamp\": 1234}", Log.class);
59+
this.objectMapper.readValue("{\"timestamp\": 1234}", Log.class);
5860
}
5961
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.springframework.cloud.sleuth.instrument.rxjava;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import org.junit.AfterClass;
6+
import org.junit.Before;
7+
import org.junit.BeforeClass;
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
12+
import org.springframework.boot.test.SpringApplicationConfiguration;
13+
import org.springframework.cloud.sleuth.Sampler;
14+
import org.springframework.cloud.sleuth.Span;
15+
import org.springframework.cloud.sleuth.SpanReporter;
16+
import org.springframework.cloud.sleuth.TraceKeys;
17+
import org.springframework.cloud.sleuth.Tracer;
18+
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.stereotype.Component;
22+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
23+
import rx.Observable;
24+
import rx.functions.Action0;
25+
import rx.plugins.SleuthRxJavaPlugins;
26+
import rx.schedulers.Schedulers;
27+
import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;
28+
29+
@RunWith(SpringJUnit4ClassRunner.class)
30+
@SpringApplicationConfiguration(classes = {
31+
SleuthRxJavaIntegrationTests.TestConfig.class})
32+
public class SleuthRxJavaIntegrationTests {
33+
34+
@Autowired
35+
Tracer tracer;
36+
@Autowired
37+
TraceKeys traceKeys;
38+
@Autowired
39+
Listener listener;
40+
@Autowired
41+
SleuthRxJavaSchedulersHook sleuthRxJavaSchedulersHook;
42+
StringBuilder caller = new StringBuilder();
43+
44+
@Before
45+
public void cleanTrace() {
46+
this.listener.getEvents().clear();
47+
}
48+
49+
@BeforeClass
50+
@AfterClass
51+
public static void cleanUp() {
52+
SleuthRxJavaPlugins.resetPlugins();
53+
}
54+
55+
@Test
56+
public void should_create_new_span_when_no_current_span_when_rx_java_action_is_executed() {
57+
Observable.defer(() -> Observable.just(
58+
(Action0) () -> this.caller = new StringBuilder("actual_action")
59+
)).subscribeOn(Schedulers.newThread()).toBlocking()
60+
.subscribe(Action0::call);
61+
62+
then(this.caller.toString()).isEqualTo("actual_action");
63+
then(this.tracer.getCurrentSpan()).isNull();
64+
then(this.listener.getEvents().size()).isEqualTo(1);
65+
then(this.listener.getEvents().get(0)).hasNameEqualTo("rxjava");
66+
then(this.listener.getEvents().get(0)).hasATag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, "rxjava");
67+
then(this.listener.getEvents().get(0)).isALocalComponentSpan();
68+
}
69+
70+
@Test
71+
public void should_continue_current_span_when_rx_java_action_is_executed() {
72+
Span spanInCurrentThread = this.tracer.createSpan("current_span");
73+
this.tracer.addTag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, "current_span");
74+
75+
Observable.defer(() -> Observable.just(
76+
(Action0) () -> this.caller = new StringBuilder("actual_action")
77+
)).subscribeOn(Schedulers.newThread()).toBlocking()
78+
.subscribe(Action0::call);
79+
80+
then(this.caller.toString()).isEqualTo("actual_action");
81+
then(this.tracer.getCurrentSpan()).isNotNull();
82+
//making sure here that no new spans were created or reported as closed
83+
then(this.listener.getEvents().size()).isEqualTo(0);
84+
then(spanInCurrentThread).hasNameEqualTo(spanInCurrentThread.getName());
85+
then(spanInCurrentThread).hasATag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, "current_span");
86+
then(spanInCurrentThread).isALocalComponentSpan();
87+
}
88+
89+
@Component
90+
public static class Listener implements SpanReporter {
91+
92+
List<Span> events = new ArrayList<>();
93+
94+
public List<Span> getEvents() {
95+
return this.events;
96+
}
97+
98+
@Override
99+
public void report(Span span) {
100+
this.events.add(span);
101+
}
102+
}
103+
104+
@Configuration
105+
@EnableAutoConfiguration
106+
public static class TestConfig {
107+
108+
@Bean
109+
Listener listener() {
110+
return new Listener();
111+
}
112+
113+
@Bean
114+
Sampler alwaysSampler() {
115+
return new AlwaysSampler();
116+
}
117+
}
118+
119+
}

0 commit comments

Comments
 (0)