|
1 | 1 | package datadog.smoketest.trace; |
2 | 2 |
|
3 | 3 | import static java.util.Comparator.comparingLong; |
| 4 | +import static java.util.stream.Collectors.toSet; |
4 | 5 |
|
5 | 6 | import datadog.trace.test.agent.decoder.DecodedSpan; |
6 | 7 | import java.util.ArrayList; |
7 | 8 | import java.util.Comparator; |
8 | 9 | import java.util.HashMap; |
9 | 10 | import java.util.List; |
10 | 11 | import java.util.Map; |
| 12 | +import java.util.Set; |
11 | 13 | import java.util.function.UnaryOperator; |
| 14 | +import org.opentest4j.AssertionFailedError; |
12 | 15 |
|
13 | 16 | /** |
14 | | - * Thin trace matcher for smoke tests: one {@link SpanMatcher} per expected span in a trace. |
15 | | - * Companion of {@link SpanMatcher}; see its class doc for the serialized-model rationale. |
| 17 | + * This class is a helper class to verify a trace structure. |
16 | 18 | * |
17 | | - * <p>Spans are matched positionally. By default they are matched in the order received; sort them |
18 | | - * first for a deterministic position: |
| 19 | + * <p>To get a {@link TraceMatcher}, use the static factory methods: {@link #trace(SpanMatcher...)} |
| 20 | + * with the expected {@link SpanMatcher}s (one per expected span), or {@link #trace(UnaryOperator, |
| 21 | + * SpanMatcher...)} to configure the checks with a {@link Options} object. |
19 | 22 | * |
20 | | - * <ul> |
21 | | - * <li>{@link #SORT_BY_START_TIME} — by span start time. Beware: spans that start within the same |
22 | | - * clock tick (e.g. a publish and its broker-side deliver) can race, so this order is <em>not |
23 | | - * stable</em> for tightly-coupled concurrent spans. |
24 | | - * <li>{@link #SORT_BY_PARENT_CHAIN} — root→leaf following parent links. Stable regardless of |
25 | | - * timestamps, but only defined for a <em>single linear chain</em> (each span has at most one |
26 | | - * child); it throws otherwise. Ideal for a straight request→...→response distributed trace. |
27 | | - * </ul> |
| 23 | + * <p>{@link #SORT_BY_START_TIME} can be used as predefined configuration to sort spans by start |
| 24 | + * time, and {@link #SORT_BY_ANCESTRY} to sort spans by ancestry, root spans (or which parents are |
| 25 | + * not present in the trace chunk) first, followed by their children by start time, depth-first. |
28 | 26 | * |
29 | | - * <p>A sorted order also makes {@link SpanMatcher#childOfIndex(int)} / {@link |
30 | | - * SpanMatcher#childOfPrevious()} deterministic. |
| 27 | + * @see SmokeTraceAssertions |
| 28 | + * @see SpanMatcher |
31 | 29 | */ |
32 | 30 | public final class TraceMatcher { |
33 | | - /** Sorts a trace's spans by start time. */ |
| 31 | + /* |
| 32 | + * Span comparators. |
| 33 | + */ |
| 34 | + /** Span comparator to sort by start time. */ |
34 | 35 | public static final Comparator<DecodedSpan> START_TIME_COMPARATOR = |
35 | | - comparingLong(DecodedSpan::getStart); |
| 36 | + comparingLong(DecodedSpan::getStart).thenComparingLong(DecodedSpan::getSpanId); |
36 | 37 |
|
37 | | - /** Configures {@link #trace(UnaryOperator, SpanMatcher...)} to sort spans by start time. */ |
| 38 | + /* |
| 39 | + * Span assertion options. |
| 40 | + */ |
| 41 | + /** Sorts spans by start time. */ |
38 | 42 | public static final UnaryOperator<Options> SORT_BY_START_TIME = |
39 | 43 | options -> options.sort(START_TIME_COMPARATOR); |
40 | 44 |
|
41 | 45 | /** |
42 | | - * Configures {@link #trace(UnaryOperator, SpanMatcher...)} to order spans root→leaf by following |
43 | | - * parent links (see {@link Options#linearizeByParentChain()}). Timestamp-independent, so unlike |
44 | | - * {@link #SORT_BY_START_TIME} it is stable for concurrent spans — but only valid for a single |
45 | | - * linear chain. |
| 46 | + * Sorts spans by ancestry, root spans (or which parents are absent from the trace chunk) first, |
| 47 | + * followed by their children by start time, depth-first. |
46 | 48 | */ |
47 | | - public static final UnaryOperator<Options> SORT_BY_PARENT_CHAIN = Options::linearizeByParentChain; |
| 49 | + public static final UnaryOperator<Options> SORT_BY_ANCESTRY = Options::sortByAncestry; |
48 | 50 |
|
49 | 51 | private final Options options; |
50 | | - private final SpanMatcher[] spanMatchers; |
| 52 | + private final SpanMatcher[] matchers; |
51 | 53 |
|
52 | 54 | private TraceMatcher(Options options, SpanMatcher[] spanMatchers) { |
53 | 55 | if (spanMatchers.length == 0) { |
54 | 56 | throw new IllegalArgumentException("No span matchers provided"); |
55 | 57 | } |
56 | 58 | this.options = options; |
57 | | - this.spanMatchers = spanMatchers; |
| 59 | + this.matchers = spanMatchers; |
58 | 60 | } |
59 | 61 |
|
60 | | - /** Checks a trace structure, one matcher per expected span, in the order received. */ |
| 62 | + /** |
| 63 | + * Checks a trace structure. |
| 64 | + * |
| 65 | + * @param matchers The matchers to verify the trace structure. |
| 66 | + */ |
61 | 67 | public static TraceMatcher trace(SpanMatcher... matchers) { |
62 | 68 | return new TraceMatcher(new Options(), matchers); |
63 | 69 | } |
64 | 70 |
|
65 | | - /** Checks a trace structure with the given options (e.g. {@link #SORT_BY_START_TIME}). */ |
| 71 | + /** |
| 72 | + * Checks a trace structure. |
| 73 | + * |
| 74 | + * @param options The {@link TraceMatcher.Options} to configure the checks. |
| 75 | + * @param matchers The matchers to verify the trace structure. |
| 76 | + */ |
66 | 77 | public static TraceMatcher trace(UnaryOperator<Options> options, SpanMatcher... matchers) { |
67 | 78 | return new TraceMatcher(options.apply(new Options()), matchers); |
68 | 79 | } |
69 | 80 |
|
70 | 81 | void assertTrace(List<DecodedSpan> spans, int traceIndex) { |
71 | | - if (spans.size() != spanMatchers.length) { |
72 | | - throw new AssertionError( |
73 | | - "Expected " |
74 | | - + spanMatchers.length |
75 | | - + " spans in trace " |
76 | | - + traceIndex |
77 | | - + " but got " |
78 | | - + spans.size() |
79 | | - + ": " |
80 | | - + spans); |
| 82 | + if (spans.size() != this.matchers.length) { |
| 83 | + throw new AssertionFailedError( |
| 84 | + "Invalid number of spans for trace " + traceIndex + " : " + spans, |
| 85 | + this.matchers.length, |
| 86 | + spans.size()); |
81 | 87 | } |
82 | | - List<DecodedSpan> ordered; |
83 | | - if (options.linearizeByParentChain) { |
84 | | - ordered = chainOrder(spans); |
85 | | - } else { |
86 | | - ordered = new ArrayList<>(spans); |
87 | | - if (options.comparator != null) { |
88 | | - ordered.sort(options.comparator); |
89 | | - } |
90 | | - } |
91 | | - for (int i = 0; i < spanMatchers.length; i++) { |
92 | | - spanMatchers[i].assertSpan(ordered, i); |
| 88 | + if (this.options.sortByAncestry) { |
| 89 | + spans = sortByAncestry(spans); |
| 90 | + } else if (this.options.comparator != null) { |
| 91 | + spans = new ArrayList<>(spans); |
| 92 | + spans.sort(this.options.comparator); |
93 | 93 | } |
94 | | - } |
95 | | - |
96 | | - /** |
97 | | - * Whether this matcher accepts {@code spans} as a whole trace, without throwing — the boolean |
98 | | - * counterpart of {@link #assertTrace} used by order-independent matching (see {@link |
99 | | - * SmokeTraceAssertions#assertTraces} with {@link SmokeTraceAssertions#IGNORE_ADDITIONAL_TRACES}), |
100 | | - * where each matcher must find some received trace it fits. A non-linear trace under {@link |
101 | | - * #SORT_BY_PARENT_CHAIN} counts as "does not match" here (rather than propagating) so probing can |
102 | | - * move on. |
103 | | - */ |
104 | | - boolean matches(List<DecodedSpan> spans, int traceIndex) { |
105 | | - try { |
106 | | - assertTrace(spans, traceIndex); |
107 | | - return true; |
108 | | - } catch (AssertionError | IllegalStateException ignored) { |
109 | | - return false; |
| 94 | + for (int i = 0; i < this.matchers.length; i++) { |
| 95 | + this.matchers[i].assertSpan(spans, i); |
110 | 96 | } |
111 | 97 | } |
112 | 98 |
|
113 | | - /** |
114 | | - * Orders spans root→leaf by following parent links. Requires a single linear chain: exactly one |
115 | | - * root (parent id 0) and every span at most one child, with all spans reachable from the root. |
116 | | - * Throws {@link IllegalStateException} otherwise, so a non-chain trace fails loudly rather than |
117 | | - * being silently mis-ordered. |
118 | | - */ |
119 | | - private static List<DecodedSpan> chainOrder(List<DecodedSpan> spans) { |
120 | | - DecodedSpan root = null; |
121 | | - Map<Long, DecodedSpan> childByParent = new HashMap<>(); |
| 99 | + private static List<DecodedSpan> sortByAncestry(List<DecodedSpan> spans) { |
| 100 | + Set<Long> spanIds = spans.stream().map(DecodedSpan::getSpanId).collect(toSet()); |
| 101 | + Map<Long, List<DecodedSpan>> spansByParentId = new HashMap<>(); |
122 | 102 | for (DecodedSpan span : spans) { |
123 | | - if (span.getParentId() == 0L) { |
124 | | - if (root != null) { |
125 | | - throw new IllegalStateException( |
126 | | - "SORT_BY_PARENT_CHAIN requires a single root span (parent id 0); found more than one"); |
127 | | - } |
128 | | - root = span; |
129 | | - } else if (childByParent.put(span.getParentId(), span) != null) { |
130 | | - throw new IllegalStateException( |
131 | | - "SORT_BY_PARENT_CHAIN requires a linear chain, but span " |
132 | | - + span.getParentId() |
133 | | - + " has more than one child"); |
| 103 | + long parentId = span.getParentId(); |
| 104 | + if (parentId != 0 && !spanIds.contains(parentId)) { |
| 105 | + parentId = 0; |
134 | 106 | } |
| 107 | + spansByParentId.computeIfAbsent(parentId, k -> new ArrayList<>()).add(span); |
135 | 108 | } |
136 | | - if (root == null) { |
137 | | - throw new IllegalStateException("SORT_BY_PARENT_CHAIN requires a root span (parent id 0)"); |
138 | | - } |
| 109 | + spansByParentId.forEach((k, v) -> v.sort(START_TIME_COMPARATOR)); |
| 110 | + |
139 | 111 | List<DecodedSpan> ordered = new ArrayList<>(spans.size()); |
140 | | - for (DecodedSpan current = root; |
141 | | - current != null; |
142 | | - current = childByParent.get(current.getSpanId())) { |
143 | | - ordered.add(current); |
144 | | - } |
145 | | - if (ordered.size() != spans.size()) { |
146 | | - throw new IllegalStateException( |
147 | | - "SORT_BY_PARENT_CHAIN: trace is not a single chain (" |
148 | | - + ordered.size() |
149 | | - + " of " |
150 | | - + spans.size() |
151 | | - + " spans reachable from the root)"); |
152 | | - } |
| 112 | + appendChildren(ordered, spansByParentId.get(0L), spansByParentId); |
153 | 113 | return ordered; |
154 | 114 | } |
155 | 115 |
|
| 116 | + private static void appendChildren( |
| 117 | + List<DecodedSpan> orderedSpan, |
| 118 | + List<DecodedSpan> children, |
| 119 | + Map<Long, List<DecodedSpan>> spansByParentId) { |
| 120 | + for (DecodedSpan child : children) { |
| 121 | + orderedSpan.add(child); |
| 122 | + List<DecodedSpan> grandChildren = spansByParentId.get(child.getSpanId()); |
| 123 | + if (grandChildren != null) { |
| 124 | + appendChildren(orderedSpan, grandChildren, spansByParentId); |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + |
156 | 129 | public static final class Options { |
157 | | - Comparator<DecodedSpan> comparator = null; // null => keep received order |
158 | | - boolean linearizeByParentChain = false; |
| 130 | + private Comparator<DecodedSpan> comparator = null; |
| 131 | + private boolean sortByAncestry = false; |
159 | 132 |
|
160 | 133 | public Options sort(Comparator<DecodedSpan> comparator) { |
161 | 134 | this.comparator = comparator; |
| 135 | + this.sortByAncestry = false; |
162 | 136 | return this; |
163 | 137 | } |
164 | 138 |
|
165 | | - /** |
166 | | - * Order spans root→leaf by parent links instead of by a comparator (see {@link |
167 | | - * #SORT_BY_PARENT_CHAIN}). |
168 | | - */ |
169 | | - public Options linearizeByParentChain() { |
170 | | - this.linearizeByParentChain = true; |
| 139 | + Options sortByAncestry() { |
| 140 | + this.comparator = null; |
| 141 | + this.sortByAncestry = true; |
171 | 142 | return this; |
172 | 143 | } |
173 | 144 | } |
|
0 commit comments