Instrument spring integration.

Via a global channel interceptor that adds headers to messages.

fixes gh-14
This commit is contained in:
Spencer Gibb
2015-08-06 15:05:34 -06:00
committed by Dave Syer
parent de60266ab5
commit 7da26be608
5 changed files with 334 additions and 3 deletions

View File

@@ -44,9 +44,8 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
<version>${spring-integration.version}</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
<optional>true</optional>
</dependency>
<dependency>

View File

@@ -0,0 +1,203 @@
/*
* 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.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.cloud.sleuth.TraceContextHolder.getCurrentSpan;
import static org.springframework.cloud.sleuth.TraceContextHolder.setCurrentSpan;
import java.util.HashMap;
import java.util.Map;
import org.springframework.aop.support.AopUtils;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.ExecutorChannelInterceptor;
import org.springframework.util.Assert;
/**
* The {@link ExecutorChannelInterceptor} implementation responsible for
* the {@link Span} propagation from one message flow's thread to another
* through the {@link MessageChannel}s involved in the flow.
* <p>
* In addition this interceptor cleans up (restores) the {@link Span}
* in the containers Threads for channels like
* {@link org.springframework.integration.channel.ExecutorChannel}
* and {@link org.springframework.integration.channel.QueueChannel}.
* @author Spencer Gibb
* @since 1.0
*/
public class TraceContextPropagationChannelInterceptor
extends ChannelInterceptorAdapter implements ExecutorChannelInterceptor, ApplicationEventPublisherAware {
private final static ThreadLocal<Span> ORIGINAL_CONTEXT = new ThreadLocal<>();
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public final Message<?> preSend(Message<?> message, MessageChannel channel) {
if (DirectChannel.class.isAssignableFrom(AopUtils.getTargetClass(channel))) {
return message;
}
//TODO: start span from headers?
Span span = getCurrentSpan();
if (span != null) {
publish(new ClientSentEvent(this, span));
return new MessageWithSpan(message, span);
}
else {
return message;
}
}
@Override
@SuppressWarnings("unchecked")
public final Message<?> postReceive(Message<?> message, MessageChannel channel) {
if (message instanceof MessageWithSpan) {
MessageWithSpan messageWithSpan = (MessageWithSpan) message;
Message<?> messageToHandle = messageWithSpan.message;
populatePropagatedContext(messageWithSpan.span, messageToHandle, channel);
publish(new ClientReceivedEvent(this, messageWithSpan.span));
return message;
}
return message;
}
@Override
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
Span originalContext = ORIGINAL_CONTEXT.get();
try {
if (originalContext == null) {
setCurrentSpan(null);
ORIGINAL_CONTEXT.remove();
}
else {
setCurrentSpan(originalContext);
}
}
catch (Throwable t) {//NOSONAR
setCurrentSpan(null);
}
}
@Override
public final Message<?> beforeHandle(Message<?> message, MessageChannel channel, MessageHandler handler) {
return postReceive(message, channel);
}
private void publish(ApplicationEvent event) {
if (this.publisher !=null) {
this.publisher.publishEvent(event);
}
}
private String getParentId(Span span) {
return span.getParents() != null && !span.getParents().isEmpty() ? span
.getParents().get(0) : null;
}
protected void populatePropagatedContext(Span span, Message<?> message,
MessageChannel channel) {
if (span != null) {
Span currentContext = getCurrentSpan();
ORIGINAL_CONTEXT.set(currentContext);
setCurrentSpan(span);
}
}
private class MessageWithSpan implements Message<Object> {
private final Message<?> message;
private final Span span;
private final MessageHeaders messageHeaders;
public MessageWithSpan(Message<?> message, Span span) {
Assert.notNull(message, "message can not be null");
Assert.notNull(span, "span can not be null");
this.message = message;
this.span = span;
Map<String, Object> headers = new HashMap<>();
headers.putAll(message.getHeaders());
setHeader(headers, SPAN_ID_NAME, this.span.getSpanId());
setHeader(headers, TRACE_ID_NAME, this.span.getTraceId());
setHeader(headers, SPAN_NAME_NAME, this.span.getName());
String parentId = getParentId(getCurrentSpan());
if (parentId != null) {
setHeader(headers, PARENT_ID_NAME, parentId);
}
String processId = this.span.getProcessId();
if (processId != null) {
setHeader(headers, PROCESS_ID_NAME, processId);
}
this.messageHeaders = new MessageHeaders(headers);
}
public void setHeader(Map<String, Object> headers, String name, String value) {
if (!headers.containsKey(name)) {
headers.put(name, value);
}
}
@Override
public Object getPayload() {
return this.message.getPayload();
}
@Override
public MessageHeaders getHeaders() {
return this.messageHeaders;
}
@Override
public String toString() {
return "MessageWithThreadState{" +
"message=" + message +
", span=" + span +
", messageHeaders=" + messageHeaders +
'}';
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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 org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.config.GlobalChannelInterceptor;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(GlobalChannelInterceptor.class)
public class TraceSpringIntegrationAutoConfiguration {
@Bean
@GlobalChannelInterceptor
@ConditionalOnProperty(value = "spring.cloud.sleuth.instrument.integration.globalChannelInterceptor.enabled", matchIfMissing = true)
public TraceContextPropagationChannelInterceptor traceContextPropagationChannelInterceptor() {
return new TraceContextPropagationChannelInterceptor();
}
}

View File

@@ -2,6 +2,7 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration,\
org.springframework.cloud.sleuth.log.SleuthLogAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.integration.TraceSpringIntegrationAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration

View File

@@ -0,0 +1,90 @@
/*
* 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.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
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;
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.context.ContextConfiguration;
/**
* @author Spencer Gibb
*/
@ContextConfiguration(classes = TraceContextPropagationChannelInterceptorTests.App.class)
public class TraceContextPropagationChannelInterceptorTests {
private ConfigurableApplicationContext context;
@After
public void close() {
if (context != null) {
context.close();
}
}
@Test
public void testSpanPropagation() {
context = SpringApplication.run(App.class);
PollableChannel channel = context.getBean("channel", PollableChannel.class);
Trace trace = context.getBean(Trace.class);
TraceScope traceScope = trace.startSpan("testSendMessage", new AlwaysSampler(), null);
channel.send(MessageBuilder.withPayload("hi").build());
traceScope.close();
Message<?> message = 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
static class App {
@Bean
public QueueChannel channel() {
return new QueueChannel();
}
}
}