From 981fbd18f1e2e2056fea6f69bae51158b916eb0f Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 5 Oct 2017 21:29:53 +0800 Subject: [PATCH 1/2] Encodes epoch seconds into first 32bits of a 128-bit trace ID (#724) Amazon will throw out trace IDs that aren't associated with a recent timestamp. This encodes the current epoch seconds into the first 32 of a 128-bit trace ID to support conversion to an Amazon Root ID. --- .../cloud/sleuth/trace/DefaultTracer.java | 19 +++++++++++++++++-- .../sleuth/trace/DefaultTracerTests.java | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/trace/DefaultTracer.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/trace/DefaultTracer.java index 76546d703..c96e96331 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/trace/DefaultTracer.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/trace/DefaultTracer.java @@ -98,7 +98,7 @@ public class DefaultTracer implements Tracer { else { long id = createId(); span = Span.builder().name(shortenedName) - .traceIdHigh(this.traceId128 ? createId() : 0L) + .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L) .traceId(id) .spanId(id).build(); if (sampler == null) { @@ -171,7 +171,7 @@ public class DefaultTracer implements Tracer { long id = createId(); if (parent == null) { Span span = Span.builder().name(shortenedName) - .traceIdHigh(this.traceId128 ? createId() : 0L) + .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L) .traceId(id) .spanId(id).build(); span = sampledSpan(span, this.defaultSampler); @@ -208,6 +208,21 @@ public class DefaultTracer implements Tracer { return span; } + /** + * Encodes a timestamp into the upper 32-bits, so that it can be converted to an Amazon trace ID. + * + *

For example, an Amazon trace ID is composed of the following: {@code |-- 32 bits for epoch + * seconds -- | -- 96 bits for random data -- |} + * + *

To support this, {@link Span#getTraceIdHigh() traceIdHigh} holds the epoch seconds and first + * 32 random bits: and {@link Span#getTraceId()} traceId} holds the remaining 64 random bits. + */ + private long createTraceIdHigh() { + long epochSeconds = System.currentTimeMillis() / 1000; + int random = this.random.nextInt(); + return (epochSeconds & 0xffffffffL) << 32 | (random & 0xffffffffL); + } + private long createId() { return this.random.nextLong(); } diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/trace/DefaultTracerTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/trace/DefaultTracerTests.java index c6a051aaf..16968e5ce 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/trace/DefaultTracerTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/trace/DefaultTracerTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.sleuth.trace; +import java.util.Date; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -231,6 +232,20 @@ public class DefaultTracerTests { tracer.close(span); } + /** + * To support conversion to Amazon trace IDs, the first 32 bits of the trace ID are epoch seconds. + */ + @Test + public void creates128bitTraceIdWithEncodedTimestamp() { + DefaultTracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(), + this.spanNamer, this.spanLogger, this.spanReporter, true, new TraceKeys()); + Span span = tracer.createSpan(bigName()); + String traceId = span.traceIdString(); + long epochSeconds = Long.parseLong(traceId.substring(0, 8), 16); + then(new Date(epochSeconds * 1000)).isToday(); + tracer.close(span); + } + @Test public void shouldCreateChildOfSpanWithShortenedName() { DefaultTracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(), From 522087ac2584155dad28fb9adf1b0c2979982209 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Fri, 6 Oct 2017 14:18:02 +0800 Subject: [PATCH 2/2] Adds spring.sleuth.supports-join to disable span ID sharing (#725) This adds the ability to opt out of span sharing between the client and server side of an RPC. This is important when reporting to systems that do not share span IDs, such as Google Stackdriver and Amazon X-Ray. --- .../sleuth/autoconfig/SleuthProperties.java | 10 +++++++ .../sleuth/instrument/web/TraceFilter.java | 13 +++++++-- ...ceFilterAlwaysSamplerIntegrationTests.java | 2 ++ .../TraceFilterMockChainIntegrationTests.java | 3 ++ .../instrument/web/TraceFilterTests.java | 28 ++++++++++++------- .../common/AbstractMvcIntegrationTest.java | 2 ++ 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthProperties.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthProperties.java index 45d790b2b..01a2e52f2 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthProperties.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthProperties.java @@ -29,6 +29,8 @@ public class SleuthProperties { private boolean enabled = true; /** When true, generate 128-bit trace IDs instead of 64-bit ones. */ private boolean traceId128 = false; + /** When true, your tracing system allows sharing a span ID between a client and server span */ + private boolean supportsJoin = true; public boolean isEnabled() { return this.enabled; @@ -45,4 +47,12 @@ public class SleuthProperties { public void setTraceId128(boolean traceId128) { this.traceId128 = traceId128; } + + public boolean isSupportsJoin() { + return this.supportsJoin; + } + + public void setSupportsJoin(boolean supportsJoin) { + this.supportsJoin = supportsJoin; + } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java index 2cda8c191..f1630ecac 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceFilter.java @@ -37,6 +37,7 @@ import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanReporter; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.SleuthProperties; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; import org.springframework.cloud.sleuth.sampler.NeverSampler; import org.springframework.core.Ordered; @@ -97,6 +98,7 @@ public class TraceFilter extends GenericFilterBean { private Tracer tracer; private TraceKeys traceKeys; private final Pattern skipPattern; + private final boolean supportsJoin; private SpanReporter spanReporter; private HttpSpanExtractor spanExtractor; private HttpTraceKeysInjector httpTraceKeysInjector; @@ -111,6 +113,7 @@ public class TraceFilter extends GenericFilterBean { public TraceFilter(BeanFactory beanFactory, Pattern skipPattern) { this.beanFactory = beanFactory; + this.supportsJoin = beanFactory.getBean(SleuthProperties.class).isSupportsJoin(); this.skipPattern = skipPattern; } @@ -344,10 +347,14 @@ public class TraceFilter extends GenericFilterBean { if (log.isDebugEnabled()) { log.debug("Found a parent span " + parent + " in the request"); } - addRequestTagsForParentSpan(request, parent); - spanFromRequest = parent; + if (!this.supportsJoin) { // create a child span for this side of the RPC + spanFromRequest = tracer().createSpan(parent.getName(), parent); + } else { + spanFromRequest = parent; + } + addRequestTagsForParentSpan(request, spanFromRequest); tracer().continueSpan(spanFromRequest); - if (parent.isRemote()) { + if (parent.isRemote()) { // then we are in a server span parent.logEvent(Span.SERVER_RECV); } request.setAttribute(TRACE_REQUEST_ATTR, spanFromRequest); diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterAlwaysSamplerIntegrationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterAlwaysSamplerIntegrationTests.java index 08f7f8354..85e3fcd81 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterAlwaysSamplerIntegrationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterAlwaysSamplerIntegrationTests.java @@ -17,6 +17,7 @@ import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanReporter; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.SleuthProperties; import org.springframework.cloud.sleuth.instrument.DefaultTestAutoConfiguration; import org.springframework.cloud.sleuth.instrument.web.common.AbstractMvcIntegrationTest; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; @@ -71,6 +72,7 @@ public class TraceFilterAlwaysSamplerIntegrationTests extends AbstractMvcIntegra BeanFactory beanFactory = Mockito.mock(BeanFactory.class); BDDMockito.given(beanFactory.getBean(TraceWebAutoConfiguration.SkipPatternProvider.class)) .willThrow(new NoSuchBeanDefinitionException("foo")); + BDDMockito.given(beanFactory.getBean(SleuthProperties.class)).willReturn(this.properties); BDDMockito.given(beanFactory.getBean(Tracer.class)).willReturn(this.tracer); BDDMockito.given(beanFactory.getBean(TraceKeys.class)).willReturn(this.traceKeys); BDDMockito.given(beanFactory.getBean(HttpSpanExtractor.class)).willReturn(this.spanExtractor); diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterMockChainIntegrationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterMockChainIntegrationTests.java index 644f05ecc..072cce768 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterMockChainIntegrationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterMockChainIntegrationTests.java @@ -30,6 +30,7 @@ import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanReporter; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.SleuthProperties; import org.springframework.cloud.sleuth.log.NoOpSpanLogger; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; import org.springframework.cloud.sleuth.trace.DefaultTracer; @@ -54,6 +55,7 @@ public class TraceFilterMockChainIntegrationTests { new Random(), new DefaultSpanNamer(), new NoOpSpanLogger(), new NoOpSpanReporter(), new TraceKeys()); private TraceKeys traceKeys = new TraceKeys(); + private SleuthProperties properties = new SleuthProperties(); private HttpTraceKeysInjector keysInjector = new HttpTraceKeysInjector(this.tracer, this.traceKeys); private MockHttpServletRequest request; @@ -94,6 +96,7 @@ public class TraceFilterMockChainIntegrationTests { private BeanFactory beanFactory() { BeanFactory beanFactory = Mockito.mock(BeanFactory.class); + BDDMockito.given(beanFactory.getBean(SleuthProperties.class)).willReturn(this.properties); BDDMockito.given(beanFactory.getBean(Tracer.class)).willReturn(this.tracer); BDDMockito.given(beanFactory.getBean(TraceKeys.class)).willReturn(this.traceKeys); BDDMockito.given(beanFactory.getBean(HttpSpanExtractor.class)) diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java index 5c3ed5dbc..82f594797 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceFilterTests.java @@ -38,6 +38,7 @@ import org.springframework.cloud.sleuth.SpanReporter; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.assertions.ListOfSpans; +import org.springframework.cloud.sleuth.autoconfig.SleuthProperties; import org.springframework.cloud.sleuth.log.SpanLogger; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; import org.springframework.cloud.sleuth.sampler.NeverSampler; @@ -67,7 +68,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder public class TraceFilterTests { public static final long PARENT_ID = 10L; - public static final String PARENT_ID_AS_STRING = String.valueOf(PARENT_ID); @Mock SpanLogger spanLogger; ArrayListSpanAccumulator spanReporter = new ArrayListSpanAccumulator(); @@ -76,6 +76,7 @@ public class TraceFilterTests { private Tracer tracer; private TraceKeys traceKeys = new TraceKeys(); + private SleuthProperties properties = new SleuthProperties(); private HttpTraceKeysInjector httpTraceKeysInjector; private Span span; @@ -200,15 +201,6 @@ public class TraceFilterTests { then(ExceptionUtils.getLastException()).isNull(); } - private Span parentSpan() { - Optional parent = this.spanReporter.getSpans().stream() - .filter(span -> Span.idToHex(span.getSpanId()).equals(PARENT_ID_AS_STRING) - || span.getName().equals("http:/parent/")) - .findFirst(); - assertThat(parent.isPresent()).isTrue(); - return parent.get(); - } - @Test public void continuesSpanInRequestAttr() throws Exception { Span span = this.tracer.createSpan("http:foo"); @@ -268,6 +260,21 @@ public class TraceFilterTests { then(TestSpanContextHolder.getCurrentSpan()).isNull(); } + @Test + public void createsChildFromHeadersWhenJoinUnsupported() throws Exception { + this.properties.setSupportsJoin(false); + this.request = builder().header(Span.SPAN_ID_NAME, PARENT_ID) + .header(Span.TRACE_ID_NAME, 20L).buildRequest(new MockServletContext()); + BeanFactory beanFactory = beanFactory(); + BDDMockito.given(beanFactory.getBean(SpanReporter.class)).willReturn(this.spanReporter); + + TraceFilter filter = new TraceFilter(beanFactory); + filter.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.spanReporter.getSpans().get(0).getParents().get(0)) + .isEqualTo(16); // test data is in hex! + } + @Test public void addsAdditionalHeaders() throws Exception { this.request = builder().header(Span.SPAN_ID_NAME, PARENT_ID) @@ -556,6 +563,7 @@ public class TraceFilterTests { private BeanFactory beanFactory() { BDDMockito.given(beanFactory.getBean(TraceWebAutoConfiguration.SkipPatternProvider.class)) .willThrow(new NoSuchBeanDefinitionException("foo")); + BDDMockito.given(beanFactory.getBean(SleuthProperties.class)).willReturn(this.properties); BDDMockito.given(beanFactory.getBean(Tracer.class)).willReturn(this.tracer); BDDMockito.given(beanFactory.getBean(TraceKeys.class)).willReturn(this.traceKeys); BDDMockito.given(beanFactory.getBean(HttpSpanExtractor.class)).willReturn(this.spanExtractor); diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/common/AbstractMvcIntegrationTest.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/common/AbstractMvcIntegrationTest.java index 7e9b4a174..e1c8b3edf 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/common/AbstractMvcIntegrationTest.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/common/AbstractMvcIntegrationTest.java @@ -2,6 +2,7 @@ package org.springframework.cloud.sleuth.instrument.web.common; import org.junit.Before; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.sleuth.autoconfig.SleuthProperties; import org.springframework.cloud.sleuth.instrument.web.HttpSpanExtractor; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; @@ -30,6 +31,7 @@ public abstract class AbstractMvcIntegrationTest { @Autowired protected WebApplicationContext webApplicationContext; protected MockMvc mockMvc; + @Autowired protected SleuthProperties properties; @Autowired protected Tracer tracer; @Autowired protected TraceKeys traceKeys; @Autowired protected HttpSpanExtractor spanExtractor;