diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptor.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptor.java new file mode 100644 index 000000000..b1e69ca34 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015 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 + * + * http://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.integration; + +import static org.springframework.cloud.sleuth.Trace.PARENT_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.PROCESS_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.SPAN_NAME_NAME; +import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; +import static org.springframework.util.StringUtils.hasText; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.sleuth.MilliSpan; +import org.springframework.cloud.sleuth.MilliSpan.MilliSpanBuilder; +import org.springframework.cloud.sleuth.NullScope; +import org.springframework.cloud.sleuth.Trace; +import org.springframework.cloud.sleuth.TraceContextHolder; +import org.springframework.cloud.sleuth.TraceScope; +import org.springframework.integration.context.IntegrationObjectSupport; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.ChannelInterceptorAdapter; + +/** + * @author Dave Syer + * + */ +public class TraceChannelInterceptor extends ChannelInterceptorAdapter { + + private final Trace trace; + + public TraceChannelInterceptor(Trace trace) { + this.trace = trace; + } + + @Override + public Message preSend(Message message, MessageChannel channel) { + if (TraceContextHolder.isTracing()) { + return message; + } + String spanId = getHeader(message, SPAN_ID_NAME); + String traceId = getHeader(message, TRACE_ID_NAME); + String name = "message/" + + ((channel instanceof IntegrationObjectSupport) ? ((IntegrationObjectSupport) channel) + .getComponentName() : channel.toString()); + TraceScope traceScope; + if (hasText(spanId) && hasText(traceId)) { + + MilliSpanBuilder span = MilliSpan.builder().traceId(traceId).spanId(spanId); + String parentId = getHeader(message, PARENT_ID_NAME); + String processId = getHeader(message, PROCESS_ID_NAME); + String parentName = getHeader(message, SPAN_NAME_NAME); + if (parentName != null) { + span.name(parentName); + } + if (processId != null) { + span.processId(processId); + } + if (parentId != null) { + span.parent(parentId); + } + span.remote(true); + + // TODO: trace description? + traceScope = this.trace.startSpan(name, span.build()); + } + else { + traceScope = this.trace.startSpan(name); + } + if (traceScope == NullScope.INSTANCE) { + return message; + } else { + Map headers = new HashMap(); + addHeader(headers, TRACE_ID_NAME, traceScope.getSpan().getTraceId()); + addHeader(headers, SPAN_ID_NAME, traceScope.getSpan().getSpanId()); + addHeader(headers, PARENT_ID_NAME, getFirst(traceScope.getSpan().getParents())); + addHeader(headers, SPAN_NAME_NAME, traceScope.getSpan().getName()); + addHeader(headers, PROCESS_ID_NAME, traceScope.getSpan().getProcessId()); + return MessageBuilder.fromMessage(message).copyHeaders(headers).build(); + } + } + + private void addHeader(Map headers, String name, String value) { + if (value!=null) { + headers.put(name, value); + } + } + + private String getFirst(List parents) { + return parents==null || parents.isEmpty() ? null : parents.get(0); + } + + private String getHeader(Message message, String name) { + return (String) message.getHeaders().get(name); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceSpringIntegrationAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceSpringIntegrationAutoConfiguration.java index 1b8c95581..f7dcbcc63 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceSpringIntegrationAutoConfiguration.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/integration/TraceSpringIntegrationAutoConfiguration.java @@ -33,12 +33,19 @@ import org.springframework.integration.config.GlobalChannelInterceptor; @ConditionalOnClass(GlobalChannelInterceptor.class) @ConditionalOnBean(Trace.class) @AutoConfigureAfter(TraceAutoConfiguration.class) +@ConditionalOnProperty(value = "spring.sleuth.integration.enabled", matchIfMissing = true) public class TraceSpringIntegrationAutoConfiguration { @Bean @GlobalChannelInterceptor - @ConditionalOnProperty(value = "spring.sleuth.integration.enabled", matchIfMissing = true) public TraceContextPropagationChannelInterceptor traceContextPropagationChannelInterceptor() { return new TraceContextPropagationChannelInterceptor(); } + + @Bean + @GlobalChannelInterceptor + public TraceChannelInterceptor traceChannelInterceptor(Trace trace) { + return new TraceChannelInterceptor(trace); + } + } diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptorTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptorTests.java new file mode 100644 index 000000000..75bffca3c --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceChannelInterceptorTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.integration; + +import static org.junit.Assert.assertNotNull; +import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.sleuth.TraceContextHolder; +import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.integration.TraceChannelInterceptorTests.App; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.MessageEndpoint; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes=App.class) +@IntegrationTest +@DirtiesContext +public class TraceChannelInterceptorTests { + + @Autowired + @Qualifier("channel") + private PollableChannel channel; + + @After + public void close() { + TraceContextHolder.setCurrentSpan(null); + } + + @Test + public void testSpanCreation() { + + this.channel.send(MessageBuilder.withPayload("hi").build()); + + Message message = this.channel.receive(0); + + assertNotNull("message was null", message); + + String spanId = message.getHeaders().get(SPAN_ID_NAME, String.class); + assertNotNull("spanId was null", spanId); + + String traceId = message.getHeaders().get(TRACE_ID_NAME, String.class); + assertNotNull("traceId was null", traceId); + } + + @Configuration + @EnableAutoConfiguration + @MessageEndpoint + @EnableIntegration + @ImportAutoConfiguration({TraceSpringIntegrationAutoConfiguration.class, TraceAutoConfiguration.class}) + static class App { + + @Bean + public QueueChannel channel() { + return new QueueChannel(); + } + + @Bean + public AlwaysSampler alwaysSampler() { + return new AlwaysSampler(); + } + + } +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceContextPropagationChannelInterceptorTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceContextPropagationChannelInterceptorTests.java index 5ce9362d6..720b71864 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceContextPropagationChannelInterceptorTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/integration/TraceContextPropagationChannelInterceptorTests.java @@ -16,18 +16,24 @@ package org.springframework.cloud.sleuth.instrument.integration; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.cloud.sleuth.Trace; +import org.springframework.cloud.sleuth.TraceContextHolder; import org.springframework.cloud.sleuth.TraceScope; +import org.springframework.cloud.sleuth.instrument.integration.TraceContextPropagationChannelInterceptorTests.App; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.annotation.MessageEndpoint; @@ -36,40 +42,44 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author Spencer Gibb */ -@ContextConfiguration(classes = TraceContextPropagationChannelInterceptorTests.App.class) +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes=App.class) +@IntegrationTest +@DirtiesContext public class TraceContextPropagationChannelInterceptorTests { - private ConfigurableApplicationContext context; + + @Autowired + @Qualifier("channel") + private PollableChannel channel; + + @Autowired + private Trace trace; @After public void close() { - if (this.context != null) { - this.context.close(); - } + TraceContextHolder.setCurrentSpan(null); } @Test public void testSpanPropagation() { - this.context = new SpringApplicationBuilder(App.class).web(false).run(); - PollableChannel channel = this.context.getBean("channel", PollableChannel.class); - - Trace trace = this.context.getBean(Trace.class); - - TraceScope traceScope = trace.startSpan("testSendMessage", new AlwaysSampler(), null); - channel.send(MessageBuilder.withPayload("hi").build()); + TraceScope traceScope = this.trace.startSpan("testSendMessage", new AlwaysSampler(), null); + this.channel.send(MessageBuilder.withPayload("hi").build()); + String expectedSpanId = traceScope.getSpan().getSpanId(); traceScope.close(); - Message message = channel.receive(0); + Message message = this.channel.receive(0); assertNotNull("message was null", message); String spanId = message.getHeaders().get(SPAN_ID_NAME, String.class); - assertNotNull("spanId was null", spanId); + assertEquals("spanId was wrong", expectedSpanId, spanId); String traceId = message.getHeaders().get(TRACE_ID_NAME, String.class); assertNotNull("traceId was null", traceId);