/** * Copyright 2015-2017 The OpenZipkin 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 zipkin.internal; import java.net.Inet6Address; import java.net.UnknownHostException; import java.util.List; import java.util.stream.Stream; import org.junit.Test; import zipkin.Annotation; import zipkin.BinaryAnnotation; import zipkin.Constants; import zipkin.Endpoint; import zipkin.Span; import zipkin.TestObjects; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static zipkin.Constants.CLIENT_RECV; import static zipkin.Constants.CLIENT_SEND; import static zipkin.Constants.LOCAL_COMPONENT; import static zipkin.Constants.SERVER_RECV; import static zipkin.Constants.SERVER_SEND; import static zipkin.TestObjects.APP_ENDPOINT; import static zipkin.TestObjects.DB_ENDPOINT; import static zipkin.TestObjects.WEB_ENDPOINT; import static zipkin.internal.CorrectForClockSkew.getClockSkew; import static zipkin.internal.CorrectForClockSkew.ipsMatch; import static zipkin.internal.CorrectForClockSkew.isLocalSpan; public class CorrectForClockSkewTest { static final long networkLatency = 10L; static final long now = System.currentTimeMillis(); Endpoint ipv6 = Endpoint.builder() .serviceName("web") // Cheat so we don't have to catch an exception here .ipv6(sun.net.util.IPAddressUtil.textToNumericFormatV6("2001:db8::c001")) .build(); Endpoint ipv4 = Endpoint.builder() .serviceName("web") .ipv4(124 << 24 | 13 << 16 | 90 << 8 | 2) .build(); Endpoint both = ipv4.toBuilder().ipv6(ipv6.ipv6).build(); @Test public void ipsMatch_falseWhenNoIp() { Endpoint noIp = Endpoint.builder().serviceName("foo").build(); assertFalse(ipsMatch(noIp, ipv4)); assertFalse(ipsMatch(noIp, ipv6)); assertFalse(ipsMatch(ipv4, noIp)); assertFalse(ipsMatch(ipv6, noIp)); } /** * Instrumentation bugs might result in spans that look like clock skew is at play. When skew * appears on the same host, we assume it is an instrumentation bug (rather than make it worse * by adjusting it!) */ @Test public void getClockSkew_mustBeOnDifferentHosts() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, WEB_ENDPOINT)) .addAnnotation(Annotation.create(20, SERVER_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(40, CLIENT_RECV, WEB_ENDPOINT)) .build(); assertThat(getClockSkew(span)).isNull(); } @Test public void getClockSkew_endpointIsServer() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, APP_ENDPOINT)) .addAnnotation(Annotation.create(20, SERVER_SEND, APP_ENDPOINT)) .addAnnotation(Annotation.create(40, CLIENT_RECV, WEB_ENDPOINT)) .build(); assertThat(getClockSkew(span).endpoint).isEqualTo(APP_ENDPOINT); } /** * Skew is relative to the server receive and centered by the difference between the server * duration and the client duration. */ @Test public void getClockSkew_includesSplitTheLatency() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, APP_ENDPOINT)) .addAnnotation(Annotation.create(20, SERVER_SEND, APP_ENDPOINT)) .addAnnotation(Annotation.create(40, CLIENT_RECV, WEB_ENDPOINT)) .build(); assertThat(getClockSkew(span).skew).isEqualTo(-15); } /** We can't currently correct async spans, where the server lets go early. */ @Test public void getClockSkew_onlyWhenClientDurationIsLongerThanServer() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, APP_ENDPOINT)) .addAnnotation(Annotation.create(20, SERVER_SEND, APP_ENDPOINT)) .addAnnotation(Annotation.create(25, CLIENT_RECV, WEB_ENDPOINT)) .build(); assertThat(getClockSkew(span)).isNull(); } @Test public void getClockSkew_basedOnServer() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, APP_ENDPOINT)) .addAnnotation(Annotation.create(20, SERVER_SEND, APP_ENDPOINT)) .addAnnotation(Annotation.create(40, CLIENT_RECV, WEB_ENDPOINT)) .build(); assertThat(getClockSkew(span).endpoint).isEqualTo(APP_ENDPOINT); } @Test public void getClockSkew_requiresCoreAnnotationsToHaveEndpoints() { Span span = Span.builder() .traceId(1L).parentId(2L).id(3L).name("") .addAnnotation(Annotation.create(20, CLIENT_SEND, null)) .addAnnotation(Annotation.create(10 /* skew */, SERVER_RECV, null)) .addAnnotation(Annotation.create(20, SERVER_SEND, null)) .addAnnotation(Annotation.create(40, CLIENT_RECV, null)) .build(); assertThat(getClockSkew(span)).isNull(); } @Test public void ipsMatch_falseWhenIpv4Different() { Endpoint different = ipv4.toBuilder() .ipv4(124 << 24 | 13 << 16 | 90 << 8 | 3).build(); assertFalse(ipsMatch(different, ipv4)); assertFalse(ipsMatch(ipv4, different)); } @Test public void ipsMatch_falseWhenIpv6Different() throws UnknownHostException { Endpoint different = ipv6.toBuilder() .ipv6(Inet6Address.getByName("2001:db8::c002").getAddress()).build(); assertFalse(ipsMatch(different, ipv6)); assertFalse(ipsMatch(ipv6, different)); } @Test public void ipsMatch_whenIpv6Match() { assertTrue(ipsMatch(ipv6, ipv6)); assertTrue(ipsMatch(both, ipv6)); assertTrue(ipsMatch(ipv6, both)); } @Test public void ipsMatch_whenIpv4Match() { assertTrue(ipsMatch(ipv4, ipv4)); assertTrue(ipsMatch(both, ipv4)); assertTrue(ipsMatch(ipv4, both)); } @Test public void spanWithSameEndPointIsLocalSpan() { assertTrue(isLocalSpan(TestObjects.TRACE.get(0))); } @Test public void spanWithLCAnnotationIsLocalSpan() { Span localSpan = localSpan(TestObjects.TRACE.get(0), WEB_ENDPOINT, 0, 0); assertTrue(isLocalSpan(localSpan)); } @Test public void clockSkewIsCorrectedIfRpcSpanSendsAfterClientReceive() { assertClockSkewIsCorrectlyApplied(50000); } @Test public void clockSkewIsCorrectedIfRpcSpanSendsBeforeClientSend() { assertClockSkewIsCorrectlyApplied(-50000); } @Test public void clockSkewIsPropagatedToLocalSpans() { long networkLatency = 10L; Span rootSpan = createRootSpan(WEB_ENDPOINT, now, 2000L); long skew = -50000L; Span rpcSpan = childSpan(rootSpan, APP_ENDPOINT, now + networkLatency, 1000L, skew); Span local = localSpan(rpcSpan, APP_ENDPOINT, rpcSpan.timestamp + 5, 200L); Span local2 = localSpan(local, APP_ENDPOINT, local.timestamp + 10, 100L); List<Span> adjustedSpans = CorrectForClockSkew.apply(asList(rpcSpan, rootSpan, local, local2)); Span adjustedLocal = getById(adjustedSpans, local.id); assertThat(local.timestamp - skew) .isEqualTo(adjustedLocal.timestamp.longValue()); Span adjustedLocal2 = getById(adjustedSpans, local2.id); assertThat(local2.timestamp - skew) .isEqualTo(adjustedLocal2.timestamp.longValue()); } static void assertClockSkewIsCorrectlyApplied(long skew) { long rpcClientSendTs = now + 50L; long dbClientSendTimestamp = now + 60 + skew; long rootDuration = 350L; long rpcDuration = 250L; long dbDuration = 40L; Span rootSpan = createRootSpan(WEB_ENDPOINT, now, rootDuration); Span rpcSpan = childSpan(rootSpan, APP_ENDPOINT, rpcClientSendTs, rpcDuration, skew); Span tierSpan = childSpan(rpcSpan, DB_ENDPOINT, dbClientSendTimestamp, dbDuration, skew); List<Span> adjustedSpans = CorrectForClockSkew.apply(asList(rpcSpan, rootSpan, tierSpan)); long id = rpcSpan.id; Span adjustedRpcSpan = getById(adjustedSpans, id); assertThat(annotationTimestamps(adjustedRpcSpan, Constants.SERVER_RECV)) .containsExactly(rpcClientSendTs + networkLatency); assertThat(annotationTimestamps(adjustedRpcSpan, Constants.CLIENT_SEND)) .containsExactly(adjustedRpcSpan.timestamp); Span adjustedTierSpan = getById(adjustedSpans, tierSpan.id); assertThat(annotationTimestamps(adjustedTierSpan, Constants.CLIENT_SEND)) .containsExactly(adjustedTierSpan.timestamp); } static Span createRootSpan(Endpoint endPoint, long begin, long duration) { return Span.builder() .traceId(1L).id(1L).name("root").timestamp(begin) .addAnnotation(Annotation.create(begin, SERVER_RECV, endPoint)) .addAnnotation(Annotation.create(begin + duration, SERVER_SEND, endPoint)) .build(); } static Span childSpan(Span parent, Endpoint to, long begin, long duration, long skew) { long spanId = parent.id + 1; Endpoint from = parent.annotations.get(0).endpoint; long networkLatency = 10L; return Span.builder() .traceId(parent.traceId).id(spanId).parentId(parent.id) .name("span" + spanId).timestamp(begin) .addAnnotation(Annotation.create(begin, CLIENT_SEND, from)) .addAnnotation(Annotation.create(begin + skew + networkLatency, SERVER_RECV, to)) .addAnnotation(Annotation.create(begin + skew + duration - networkLatency, SERVER_SEND, to)) .addAnnotation(Annotation.create(begin + duration, CLIENT_RECV, from)) .build(); } static Span localSpan(Span parent, Endpoint endpoint, long begin, long duration) { long spanId = parent.id + 1; return Span.builder().traceId(parent.traceId).parentId(parent.id).id(spanId) .name("lc" + spanId) .timestamp(begin).duration(duration) .addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "lc" + spanId, endpoint)) .build(); } static Stream<Long> annotationTimestamps(Span span, String annotation) { return span.annotations.stream() .filter(a -> a.value.equals(annotation)) .map(a -> a.timestamp); } static Span getById(List<Span> adjustedSpans, long id) { return adjustedSpans.stream().filter(s -> s.id == id).findFirst().get(); } }