com.palantir.tracing.SpanAnalyzer Maven / Gradle / Ivy
Show all versions of tracing-test-utils Show documentation
/*
* (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();
}
}