Skip to content

Commit 81d5361

Browse files
authored
Ensures Span.timestamp/duration are not reported on remote spans (#443)
This ensures spans that have remote set to true don't send Span.timestamp/duration. In the RPC span model, the client owns the timestamp and duration of the span. If we were propagated an id, we can assume that we shouldn't report timestamp or duration, rather let the client do that. Worst case we were propagated an unreported ID and Zipkin backfills timestamp and duration.
1 parent cec661e commit 81d5361

File tree

6 files changed

+103
-29
lines changed

6 files changed

+103
-29
lines changed

spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/src/test/java/integration/MessagingApplicationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private void thenTheSpansHaveProperParentStructure() {
114114
Optional<Span> eventReceivedSpan = findSpanWithAnnotation(Constants.CLIENT_RECV);
115115
Optional<Span> lastHttpSpansParent = findLastHttpSpansParent();
116116
// "http:/parent/" -> "home" -> "message:messages" -> "http:/foo" (CS + CR) -> "http:/foo" (SS) -> "foo"
117-
Collections.sort(this.integrationTestSpanCollector.hashedSpans, (s1, s2) -> s1.timestamp.compareTo(s2.timestamp));
117+
Collections.sort(this.integrationTestSpanCollector.hashedSpans);
118118
thenAllSpansArePresent(firstHttpSpan, eventSpans, lastHttpSpansParent, eventSentSpan, eventReceivedSpan);
119119
then(this.integrationTestSpanCollector.hashedSpans).as("There were 6 spans").hasSize(6);
120120
log.info("Checking the parent child structure");

spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ConvertToZipkinSpanList.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,15 @@ static zipkin.Span convert(Span span, Host host) {
8989
if (hasClientSend(span)) {
9090
ensureServerAddr(span, zipkinSpan, ep);
9191
}
92-
zipkinSpan.timestamp(span.getBegin() * 1000);
93-
if (!span.isRunning()) { // duration is authoritative, only write when the span stopped
94-
zipkinSpan.duration(calculateDurationInMicros(span));
92+
// In the RPC span model, the client owns the timestamp and duration of the span. If we
93+
// were propagated an id, we can assume that we shouldn't report timestamp or duration,
94+
// rather let the client do that. Worst case we were propagated an unreported ID and
95+
// Zipkin backfills timestamp and duration.
96+
if (!span.isRemote()) {
97+
zipkinSpan.timestamp(span.getBegin() * 1000);
98+
if (!span.isRunning()) { // duration is authoritative, only write when the span stopped
99+
zipkinSpan.duration(calculateDurationInMicros(span));
100+
}
95101
}
96102
zipkinSpan.traceId(span.getTraceId());
97103
if (span.getParents().size() > 0) {
@@ -174,4 +180,4 @@ private static org.springframework.cloud.sleuth.Log hasLog(String logName, Span
174180
}
175181
return null;
176182
}
177-
}
183+
}

spring-cloud-sleuth-zipkin-stream/src/test/java/org/springframework/cloud/sleuth/zipkin/stream/ConvertToZipkinSpanListTests.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public void setsTheDurationToTheDifferenceBetweenCRandCS()
155155
/** Zipkin's duration should only be set when the span is finished. */
156156
@Test
157157
public void doesntSetDurationWhenStillRunning() {
158-
Span running = Span.builder().traceId(1L).name("http:parent").remote(true).build();
158+
Span running = Span.builder().traceId(1L).name("http:child").build();
159159
Spans spans = new Spans(this.host, Collections.singletonList(running));
160160
zipkin.Span result = ConvertToZipkinSpanList.convert(spans).get(0);
161161

@@ -165,9 +165,31 @@ public void doesntSetDurationWhenStillRunning() {
165165
.isNull();
166166
}
167167

168+
/**
169+
* In the RPC span model, the client owns the timestamp and duration of the span. If we
170+
* were propagated an id, we can assume that we shouldn't report timestamp or duration,
171+
* rather let the client do that. Worst case we were propagated an unreported ID and
172+
* Zipkin backfills timestamp and duration.
173+
*/
174+
@Test
175+
public void doesntSetTimestampOrDurationWhenRemote() {
176+
Span span = span("foo", true);
177+
Spans spans = new Spans(this.host, Collections.singletonList(span));
178+
zipkin.Span result = ConvertToZipkinSpanList.convert(spans).get(0);
179+
180+
assertThat(result.timestamp)
181+
.isNull();
182+
assertThat(result.duration)
183+
.isNull();
184+
}
185+
168186
Span span(String name) {
187+
return span(name, false);
188+
}
189+
190+
Span span(String name, boolean remote) {
169191
Long id = new Random().nextLong();
170-
return new Span(1, 3, "message:" + name, id, Collections.<Long>emptyList(), id, true, true,
192+
return new Span(1, 3, "message:" + name, id, Collections.<Long>emptyList(), id, remote, true,
171193
"process");
172194
}
173-
}
195+
}

spring-cloud-sleuth-zipkin-stream/src/test/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinMessageListenerTests.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,37 @@ public class ZipkinMessageListenerTests {
3434
.ipv4(1 << 24 | 2 << 16 | 3 << 8 | 4)
3535
.port(8080).build();
3636

37+
/**
38+
* In the RPC span model, the client owns the timestamp and duration of the span. If we
39+
* were propagated an id, we can assume that we shouldn't report timestamp or duration,
40+
* rather let the client do that. Worst case we were propagated an unreported ID and
41+
* Zipkin backfills timestamp and duration.
42+
*/
43+
@Test
44+
public void doesntSetTimestampOrDurationWhenRemote() {
45+
this.span.stop();
46+
zipkin.Span result = ConvertToZipkinSpanList.convert(this.span, this.host);
47+
48+
assertThat(result.timestamp)
49+
.isNull();
50+
assertThat(result.duration)
51+
.isNull();
52+
}
53+
3754
/** Sleuth timestamps are millisecond granularity while zipkin is microsecond. */
3855
@Test
3956
public void convertsTimestampAndDurationToMicroseconds() {
57+
Span span = new Span(1, 3, "http:name", 1L, Collections.<Long>emptyList(), 2L, false, true,
58+
"process");
4059
long start = System.currentTimeMillis();
41-
this.span.logEvent("hystrix/retry"); // System.currentTimeMillis
60+
span.logEvent("hystrix/retry"); // System.currentTimeMillis
4261

43-
zipkin.Span result = ConvertToZipkinSpanList.convert(this.span, this.host);
62+
zipkin.Span result = ConvertToZipkinSpanList.convert(span, this.host);
4463

4564
assertThat(result.timestamp)
46-
.isEqualTo(this.span.getBegin() * 1000);
65+
.isEqualTo(span.getBegin() * 1000);
4766
assertThat(result.duration)
48-
.isEqualTo((this.span.getEnd() - this.span.getBegin()) * 1000);
67+
.isEqualTo((span.getEnd() - span.getBegin()) * 1000);
4968
assertThat(result.annotations.get(0).timestamp)
5069
.isGreaterThanOrEqualTo(start * 1000)
5170
.isLessThanOrEqualTo(System.currentTimeMillis() * 1000);

spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinSpanListener.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,15 @@ zipkin.Span convert(Span span) {
8585
if (hasClientSend(span)) {
8686
ensureServerAddr(span, zipkinSpan);
8787
}
88-
zipkinSpan.timestamp(span.getBegin() * 1000L);
89-
if (!span.isRunning()) { // duration is authoritative, only write when the span stopped
90-
zipkinSpan.duration(calculateDurationInMicros(span));
88+
// In the RPC span model, the client owns the timestamp and duration of the span. If we
89+
// were propagated an id, we can assume that we shouldn't report timestamp or duration,
90+
// rather let the client do that. Worst case we were propagated an unreported ID and
91+
// Zipkin backfills timestamp and duration.
92+
if (!span.isRemote()) {
93+
zipkinSpan.timestamp(span.getBegin() * 1000L);
94+
if (!span.isRunning()) { // duration is authoritative, only write when the span stopped
95+
zipkinSpan.duration(calculateDurationInMicros(span));
96+
}
9197
}
9298
zipkinSpan.traceId(span.getTraceId());
9399
if (span.getParents().size() > 0) {

spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin/ZipkinSpanListenerTests.java

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.cloud.sleuth.zipkin;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.List;
2122
import javax.annotation.PostConstruct;
2223

@@ -64,16 +65,17 @@ public void init() {
6465
/** Sleuth timestamps are millisecond granularity while zipkin is microsecond. */
6566
@Test
6667
public void convertsTimestampToMicrosecondsAndSetsDurationToAccumulatedMicros() {
68+
Span span = Span.builder().traceId(1L).name("http:api").build();
6769
long start = System.currentTimeMillis();
68-
this.parent.logEvent("hystrix/retry"); // System.currentTimeMillis
69-
this.parent.stop();
70+
span.logEvent("hystrix/retry"); // System.currentTimeMillis
71+
span.stop();
7072

71-
zipkin.Span result = this.spanReporter.convert(this.parent);
73+
zipkin.Span result = this.spanReporter.convert(span);
7274

7375
assertThat(result.timestamp)
74-
.isEqualTo(this.parent.getBegin() * 1000);
76+
.isEqualTo(span.getBegin() * 1000);
7577
assertThat(result.duration)
76-
.isEqualTo(this.parent.getAccumulatedMicros());
78+
.isEqualTo(span.getAccumulatedMicros());
7779
assertThat(result.annotations.get(0).timestamp)
7880
.isGreaterThanOrEqualTo(start * 1000)
7981
.isLessThanOrEqualTo(System.currentTimeMillis() * 1000);
@@ -82,36 +84,55 @@ public void convertsTimestampToMicrosecondsAndSetsDurationToAccumulatedMicros()
8284
@Test
8385
public void setsTheDurationToTheDifferenceBetweenCRandCS()
8486
throws InterruptedException {
85-
this.parent.logEvent(Span.CLIENT_SEND);
87+
Span span = Span.builder().traceId(1L).name("http:api").build();
88+
span.logEvent(Span.CLIENT_SEND);
8689
Thread.sleep(10);
87-
this.parent.logEvent(Span.CLIENT_RECV);
90+
span.logEvent(Span.CLIENT_RECV);
8891
Thread.sleep(20);
89-
this.parent.stop();
92+
span.stop();
9093

91-
zipkin.Span result = this.spanReporter.convert(this.parent);
94+
zipkin.Span result = this.spanReporter.convert(span);
9295

9396
assertThat(result.timestamp)
94-
.isEqualTo(this.parent.getBegin() * 1000);
95-
long clientSendTimestamp = this.parent.logs().stream().filter(log -> Span.CLIENT_SEND.equals(log.getEvent()))
97+
.isEqualTo(span.getBegin() * 1000);
98+
long clientSendTimestamp = span.logs().stream().filter(log -> Span.CLIENT_SEND.equals(log.getEvent()))
9699
.findFirst().get().getTimestamp();
97-
long clientRecvTimestamp = this.parent.logs().stream().filter(log -> Span.CLIENT_RECV.equals(log.getEvent()))
100+
long clientRecvTimestamp = span.logs().stream().filter(log -> Span.CLIENT_RECV.equals(log.getEvent()))
98101
.findFirst().get().getTimestamp();
99102
assertThat(result.duration)
100-
.isNotEqualTo(this.parent.getAccumulatedMicros())
103+
.isNotEqualTo(span.getAccumulatedMicros())
101104
.isEqualTo((clientRecvTimestamp - clientSendTimestamp) * 1000);
102105
}
103106

104107
/** Zipkin's duration should only be set when the span is finished. */
105108
@Test
106109
public void doesntSetDurationWhenStillRunning() {
107-
zipkin.Span result = this.spanReporter.convert(this.parent);
110+
Span span = Span.builder().traceId(1L).name("http:api").build();
111+
zipkin.Span result = this.spanReporter.convert(span);
108112

109113
assertThat(result.timestamp)
110114
.isGreaterThan(0); // sanity check it did start
111115
assertThat(result.duration)
112116
.isNull();
113117
}
114118

119+
/**
120+
* In the RPC span model, the client owns the timestamp and duration of the span. If we
121+
* were propagated an id, we can assume that we shouldn't report timestamp or duration,
122+
* rather let the client do that. Worst case we were propagated an unreported ID and
123+
* Zipkin backfills timestamp and duration.
124+
*/
125+
@Test
126+
public void doesntSetTimestampOrDurationWhenRemote() {
127+
this.parent.stop();
128+
zipkin.Span result = this.spanReporter.convert(this.parent);
129+
130+
assertThat(result.timestamp)
131+
.isNull();
132+
assertThat(result.duration)
133+
.isNull();
134+
}
135+
115136
/** Sleuth host corresponds to annotation/binaryAnnotation.host in zipkin. */
116137
@Test
117138
public void annotationsIncludeHost() {

0 commit comments

Comments
 (0)