All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.tracing.SpanAnalyzer Maven / Gradle / Ivy

The newest version!
/*
 * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
 *
 * 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 com.palantir.tracing;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.graph.EndpointPair;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.ImmutableGraph;
import com.palantir.tracing.api.Span;
import com.palantir.tracing.api.SpanType;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.immutables.value.Value;

final class SpanAnalyzer {

    private static final String SYNTHETIC_ROOT_SPAN_ID = "SYNTHETIC_ROOT_SPAN_ID";

    private SpanAnalyzer() {}

    private static Stream depthFirstTraversalOrderedByStartTime(ImmutableGraph graph, Span parentSpan) {
        Stream children =
                children(graph, parentSpan).flatMap(child -> depthFirstTraversalOrderedByStartTime(graph, child));

        return Stream.concat(Stream.of(parentSpan), children);
    }

    public static Stream children(ImmutableGraph graph, Span parentSpan) {
        return graph.incidentEdges(parentSpan).stream()
                // we only care about incoming edges to the 'parentSpan', not outgoing ones
                .filter(pair -> pair.nodeV().equals(parentSpan))
                .map(EndpointPair::nodeU)
                .sorted(SpanComparator.INSTANCE);
    }

    public static Result analyze(Collection spans) {
        TimeBounds bounds = TimeBounds.fromSpans(spans);
        Span fakeRootSpan = createFakeRootSpan(bounds);

        Set collisions = new HashSet<>();
        Map spansBySpanId = spans.stream()
                .collect(Collectors.toMap(
                        Span::getSpanId,
                        Function.identity(),
                        (left, right) -> {
                            collisions.add(left);
                            collisions.add(right);
                            return left;
                        },
                        LinkedHashMap::new));

        Set parentlessSpans = spansBySpanId.values().stream()
                .filter(span -> span.getParentSpanId().isPresent())
                .collect(ImmutableSet.toImmutableSet());

        // We want to ensure that there is always a single root span to base our graph traversal off of
        Span rootSpan;
        if (parentlessSpans.size() != 1) {
            rootSpan = fakeRootSpan;
        } else {
            rootSpan = Iterables.getOnlyElement(parentlessSpans);
        }

        // people do crazy things with traces - they might have a trace already initialized which doesn't
        // get closed (and therefore emitted) by the time we need to render, so just hook it up to the fake
        ImmutableGraph.Builder graph = GraphBuilder.directed().immutable();
        spans.forEach(graph::addNode);
        spans.stream()
                .filter(span -> !span.getSpanId().equals(rootSpan.getSpanId()))
                .forEach(span -> graph.putEdge(
                        span, span.getParentSpanId().map(spansBySpanId::get).orElse(fakeRootSpan)));
        ImmutableGraph spanGraph = graph.build();

        return ImmutableResult.builder()
                .graph(spanGraph)
                .root(rootSpan)
                .collisions(collisions)
                .bounds(bounds)
                .build();
    }

    public static Map analyzeByTraceId(Collection spans) {
        Map> spansByTraceId = spans.stream().collect(Collectors.groupingBy(Span::getTraceId));

        return Maps.transformValues(spansByTraceId, SpanAnalyzer::analyze);
    }

    static Stream compareSpansRecursively(Result expected, Result actual, Span ex, Span ac) {
        if (!ex.getOperation().equals(ac.getOperation())) {
            return Stream.of(ComparisonFailure.unequalOperation(ex, ac));
        }
        // other fields, type, params, metadata(???)

        // ensure we have the same number of children, same child operation names in the same order
        List sortedExpectedChildren = sortedChildren(expected.graph(), ex);
        List sortedActualChildren = sortedChildren(actual.graph(), ac);
        if (sortedExpectedChildren.size() != sortedActualChildren.size()) {
            // just highlighting the parents for now.
            return Stream.of(ComparisonFailure.unequalChildren(ex, ac, sortedExpectedChildren, sortedActualChildren));
        }

        boolean expectedContainsOverlappingSpans = containsOverlappingSpans(sortedExpectedChildren);
        boolean actualContainsOverlappingSpans = containsOverlappingSpans(sortedActualChildren);
        if (expectedContainsOverlappingSpans ^ actualContainsOverlappingSpans) {
            // Either Both or neither tree should have concurrent spans
            return Stream.of(ComparisonFailure.incompatibleStructure(ex, ac));
        }

        if (!actualContainsOverlappingSpans) {
            return IntStream.range(0, sortedActualChildren.size())
                    .mapToObj(i -> compareSpansRecursively(
                            expected, actual, sortedExpectedChildren.get(i), sortedActualChildren.get(i)))
                    .flatMap(Function.identity());
        }

        if (!compatibleOverlappingSpans(expected, actual, sortedExpectedChildren, sortedActualChildren)) {
            return Stream.of(ComparisonFailure.unequalChildren(ex, ac, sortedExpectedChildren, sortedActualChildren));
        }
        return Stream.empty();
    }

    /**
     * When async spans are involved, there can be many overlapping children with the same operation name. We
     * exhaustively check each possible pair, and require that each span in the 'expected' list lines up with something
     * and each span in the 'actual' list also lines up with something.
     *
     * 

It's OK for some spans to be compatible with more than one span (as subtrees could be identical). */ private static boolean compatibleOverlappingSpans(Result expected, Result actual, List ex, List ac) { boolean[][] compatibility = new boolean[ex.size()][ac.size()]; for (int exIndex = 0; exIndex < ex.size(); exIndex++) { for (int acIndex = 0; acIndex < ac.size(); acIndex++) { long numFailures = compareSpansRecursively(expected, actual, ex.get(exIndex), ac.get(acIndex)) .count(); compatibility[exIndex][acIndex] = numFailures == 0; } } // check rows first for (int exIndex = 0; exIndex < ex.size(); exIndex++) { boolean atLeastOneCompatible = false; for (int acIndex = 0; acIndex < ac.size(); acIndex++) { atLeastOneCompatible |= compatibility[exIndex][acIndex]; } if (!atLeastOneCompatible) { return false; } } // check columns for (int acIndex = 0; acIndex < ac.size(); acIndex++) { boolean atLeastOneCompatible = false; for (int exIndex = 0; exIndex < ex.size(); exIndex++) { atLeastOneCompatible |= compatibility[exIndex][acIndex]; } if (!atLeastOneCompatible) { return false; } } return true; } /* Assumes list of spans to be ordered by startTimeMicros */ private static boolean containsOverlappingSpans(List spans) { for (int i = 0; i < spans.size() - 1; i++) { Span currentSpan = spans.get(i); Span nextSpan = spans.get(i + 1); if (nextSpan.getStartTimeMicroSeconds() < getEndTimeMicroSeconds(currentSpan)) { return true; } } return false; } private static long getEndTimeMicroSeconds(Span span) { return span.getStartTimeMicroSeconds() + (span.getDurationNanoSeconds() * 1000); } public static boolean isSyntheticRoot(Span span) { return span.getSpanId().equals(SYNTHETIC_ROOT_SPAN_ID); } @Value.Immutable interface Result { ImmutableGraph graph(); Span root(); Set collisions(); TimeBounds bounds(); @Value.Lazy default ImmutableList orderedSpans() { return depthFirstTraversalOrderedByStartTime(graph(), root()).collect(ImmutableList.toImmutableList()); } } private static List sortedChildren(ImmutableGraph graph, Span node) { return children(graph, node) .sorted(Comparator.comparingLong(Span::getStartTimeMicroSeconds)) .collect(ImmutableList.toImmutableList()); } /** Synthesizes a root span which encapsulates all known spans. */ private static Span createFakeRootSpan(TimeBounds bounds) { return Span.builder() .type(SpanType.LOCAL) .startTimeMicroSeconds(bounds.startMicros()) .durationNanoSeconds(bounds.endNanos() - bounds.startNanos()) .spanId(SYNTHETIC_ROOT_SPAN_ID) .traceId("???") .operation("") .build(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy