/** * 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.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import zipkin.Annotation; import zipkin.BinaryAnnotation; import zipkin.Constants; import zipkin.Endpoint; import zipkin.Span; /** * Adjusts spans whose children happen before their parents, based on core annotation values. */ public final class CorrectForClockSkew { static class ClockSkew { final Endpoint endpoint; final long skew; public ClockSkew(Endpoint endpoint, long skew) { this.endpoint = endpoint; this.skew = skew; } } public static List<Span> apply(List<Span> spans) { for (Span s : spans) { if (s.parentId == null) { Node<Span> tree = Node.constructTree(spans); adjust(tree, null); List<Span> result = new ArrayList<>(spans.size()); for (Iterator<Node<Span>> i = tree.traverse(); i.hasNext();) { result.add(i.next().value()); } return result; } } return spans; } /** * Recursively adjust the timestamps on the span tree. Root span is the reference point, all * children's timestamps gets adjusted based on that span's timestamps. */ static void adjust(Node<Span> node, @Nullable ClockSkew skewFromParent) { // adjust skew for the endpoint brought over from the parent span if (skewFromParent != null) { node.value(adjustTimestamps(node.value(), skewFromParent)); } // Is there any skew in the current span? ClockSkew skew = getClockSkew(node.value()); if (skew != null) { // the current span's skew may be a different endpoint than skewFromParent, adjust again. node.value(adjustTimestamps(node.value(), skew)); } else { if (skewFromParent != null && isLocalSpan(node.value())) { //Propagate skewFromParent to local spans skew = skewFromParent; } } // propagate skew to any children for (Node<Span> child : node.children()) { adjust(child, skew); } } static boolean isLocalSpan(Span span) { Endpoint endPoint = null; for (int i = 0, length = span.annotations.size(); i < length; i++) { Annotation annotation = span.annotations.get(i); if (endPoint == null) { endPoint = annotation.endpoint; } if (endPoint != null && !endPoint.equals(annotation.endpoint)) { return false; } } for (int i = 0, length = span.binaryAnnotations.size(); i < length; i++) { BinaryAnnotation binaryAnnotation = span.binaryAnnotations.get(i); if (endPoint == null) { endPoint = binaryAnnotation.endpoint; } if (endPoint != null && !endPoint.equals(binaryAnnotation.endpoint)) { return false; } } return true; } /** If any annotation has an IP with skew associated, adjust accordingly. */ static Span adjustTimestamps(Span span, ClockSkew skew) { List<Annotation> annotations = null; Long annotationTimestamp = null; for (int i = 0, length = span.annotations.size(); i < length; i++) { Annotation a = span.annotations.get(i); if (a.endpoint == null) continue; if (ipsMatch(skew.endpoint, a.endpoint)) { if (annotations == null) annotations = new ArrayList<>(span.annotations); if (span.timestamp!= null && a.timestamp == span.timestamp) { annotationTimestamp = a.timestamp; } annotations.set(i, a.toBuilder().timestamp(a.timestamp - skew.skew).build()); } } if (annotations != null) { Span.Builder builder = span.toBuilder().annotations(annotations); if (annotationTimestamp != null) { builder.timestamp(annotationTimestamp - skew.skew); } return builder.build(); } // Search for a local span on the skewed endpoint for (int i = 0, length = span.binaryAnnotations.size(); i < length; i++) { BinaryAnnotation b = span.binaryAnnotations.get(i); if (b.endpoint == null) continue; if (b.key.equals(Constants.LOCAL_COMPONENT) && ipsMatch(skew.endpoint, b.endpoint)) { return span.toBuilder().timestamp(span.timestamp - skew.skew).build(); } } return span; } static boolean ipsMatch(Endpoint skew, Endpoint that) { return (skew.ipv6 != null && Arrays.equals(skew.ipv6, that.ipv6)) || (skew.ipv4 != 0 && skew.ipv4 == that.ipv4); } /** Use client/server annotations to determine if there's clock skew. */ @Nullable static ClockSkew getClockSkew(Span span) { Map<String, Annotation> annotations = asMap(span.annotations); Annotation clientSend = annotations.get(Constants.CLIENT_SEND); Annotation clientRecv = annotations.get(Constants.CLIENT_RECV); Annotation serverRecv = annotations.get(Constants.SERVER_RECV); Annotation serverSend = annotations.get(Constants.SERVER_SEND); if (clientSend == null || clientRecv == null || serverRecv == null || serverSend == null) { return null; } Endpoint server = serverRecv.endpoint != null ? serverRecv.endpoint: serverSend.endpoint; if (server == null) return null; Endpoint client = clientSend.endpoint != null ? clientSend.endpoint: clientRecv.endpoint; if (client == null) return null; // There's no skew if the RPC is going to itself if (ipsMatch(server, client)) return null; long clientDuration = clientRecv.timestamp - clientSend.timestamp; long serverDuration = serverSend.timestamp - serverRecv.timestamp; // We assume latency is half the difference between the client and server duration. // This breaks if client duration is smaller than server (due to async return for example). if (clientDuration < serverDuration) return null; long latency = (clientDuration - serverDuration) / 2; // We can't see skew when send happens before receive if (latency < 0) return null; long skew = serverRecv.timestamp - latency - clientSend.timestamp; if (skew != 0L) { return new ClockSkew(server, skew); } return null; } /** Get the annotations as a map with value to annotation bindings. */ static Map<String, Annotation> asMap(List<Annotation> annotations) { Map<String, Annotation> result = new LinkedHashMap<>(annotations.size()); for (Annotation a : annotations) { result.put(a.value, a); } return result; } private CorrectForClockSkew() { } }