Instrument spring integration.
Via a global channel interceptor that adds headers to messages. fixes gh-14
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user