Skip to content

Commit e82387a

Browse files
committed
WIP
1 parent cc6f665 commit e82387a

4 files changed

Lines changed: 123 additions & 145 deletions

File tree

dd-smoke-tests/spring-boot-rabbit/src/test/java/datadog/smoketest/SpringBootRabbitSmokeTest.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package datadog.smoketest;
22

33
import static datadog.smoketest.trace.SpanMatcher.span;
4-
import static datadog.smoketest.trace.TraceMatcher.SORT_BY_PARENT_CHAIN;
4+
import static datadog.smoketest.trace.TraceMatcher.SORT_BY_ANCESTRY;
55
import static datadog.smoketest.trace.TraceMatcher.trace;
66
import static org.junit.jupiter.api.Assertions.assertEquals;
77

@@ -58,19 +58,19 @@
5858
* {@linkplain SmokeTraceAssertions order-independent subset} mode ({@code
5959
* unordered().ignoreAdditionalTraces()}): each matcher matches a distinct received trace and extras
6060
* are ignored. This is stronger than the Groovy set-membership check — each round-trip trace is
61-
* matched count-exact (all 12 spans, in {@link TraceMatcher#SORT_BY_PARENT_CHAIN parent-chain
62-
* order}), verifying every AMQP operation (publish/deliver/consume, both directions) <em>and</em>
63-
* its cross-service linkage — while staying robust to the timing-dependent extras (the broker emits
64-
* its connection-setup commands and per-ack traces as their own single-span traces, in
61+
* matched count-exact (all 12 spans, in {@link TraceMatcher#SORT_BY_ANCESTRY ancestry order}),
62+
* verifying every AMQP operation (publish/deliver/consume, both directions) <em>and</em> its
63+
* cross-service linkage — while staying robust to the timing-dependent extras (the broker emits its
64+
* connection-setup commands and per-ack traces as their own single-span traces, in
6565
* non-deterministic count and order).
6666
*
6767
* <p>Two design points, informed by inspecting the live trace collection:
6868
*
6969
* <ul>
70-
* <li><b>Parent-chain order, not start time</b> — the 12-span round-trip is a strict linear
71-
* chain, but its spans start within the same tick and race, so {@code SORT_BY_START_TIME} is
72-
* unstable across runs. {@code SORT_BY_PARENT_CHAIN} orders spans by parent links
73-
* (timestamp-independent) and asserts the trace is exactly that chain.
70+
* <li><b>Ancestry order, not start time</b> — the 12-span round-trip is a strict linear chain,
71+
* but its spans start within the same tick and race, so {@code SORT_BY_START_TIME} is
72+
* unstable across runs. {@code SORT_BY_ANCESTRY} orders each parent before its child
73+
* (timestamp-independent along the chain), giving a stable positional order.
7474
* <li><b>Accumulate, don't isolate</b> — {@code .retainAcrossTests()} keeps traces from app
7575
* startup onward, because {@code basic.qos}/{@code basic.consume}/{@code queue.declare} are
7676
* emitted when the consumers start (before any test method), which a per-method session
@@ -145,12 +145,12 @@ void roundtripsProduceFullAmqpTraceStructure() throws IOException {
145145
}
146146

147147
// The full distributed round-trip as one strict parent->child chain across both services and the
148-
// broker. SORT_BY_PARENT_CHAIN orders the spans root->leaf and asserts the trace IS exactly this
149-
// linear chain: HTTP entrypoint -> publish -> receiver consumes+forwards -> sender consumes
150-
// reply.
148+
// broker. SORT_BY_ANCESTRY orders the spans parent->child (stable for a linear chain), and the 12
149+
// count-exact positional matchers pin the shape: HTTP entrypoint -> publish -> receiver
150+
// consumes+forwards -> sender consumes reply.
151151
private static TraceMatcher roundTrip() {
152152
return trace(
153-
SORT_BY_PARENT_CHAIN,
153+
SORT_BY_ANCESTRY,
154154
sp("spring-rabbit-0", "servlet.request", "GET /roundtrip/{message}").root(),
155155
sp("spring-rabbit-0", "spring.handler", "WebController.roundtrip"),
156156
sp("spring-rabbit-0", "amqp.command", "basic.publish <default> -> otherqueue"),

dd-smoke-tests/src/main/java/datadog/smoketest/trace/SmokeTraceAssertions.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@
2828
* <p>A single matching algorithm is tuned by three combinable {@link Options}:
2929
*
3030
* <ul>
31-
* <li>{@link #SORT_BY_START_TIME} (the {@code sorter}) — sort the received traces before matching,
32-
* making positional matching deterministic.
31+
* <li>{@link #SORT_BY_START_TIME} (the {@code sorter}) — sort the received traces before
32+
* matching, making positional matching deterministic.
3333
* <li>{@link #UNORDERED} — a matcher may match <em>any</em> distinct (unconsumed) trace rather
34-
* than the one at its position; the received order stops mattering (so a {@code sorter} has no
35-
* effect in this mode).
34+
* than the one at its position; the received order stops mattering (so a {@code sorter} has
35+
* no effect in this mode).
3636
* <li>{@link #IGNORE_ADDITIONAL_TRACES} — don't require every received trace to be matched; extra
3737
* traces are ignored (a subset assertion). Without it, the counts must be equal.
3838
* </ul>
@@ -54,18 +54,24 @@
5454
* test-agent), both decoded to {@link DecodedTrace}.
5555
*/
5656
public final class SmokeTraceAssertions {
57-
/** Sorts traces by the earliest span start time. */
57+
/*
58+
* Trace comparators.
59+
*/
60+
/** Trace comparator to sort by start time. */
5861
public static final Comparator<DecodedTrace> TRACE_START_TIME_COMPARATOR =
5962
Comparator.comparingLong(SmokeTraceAssertions::earliestStart);
6063

61-
/** Don't require every received trace to be matched — ignore the extras (a subset assertion). */
64+
/*
65+
* Trace assertion options.
66+
*/
67+
/** Ignores additional traces. If there are more traces than expected, do not fail. */
6268
public static final UnaryOperator<Options> IGNORE_ADDITIONAL_TRACES =
6369
Options::ignoreAdditionalTraces;
6470

65-
/** Let each matcher match any distinct trace rather than the one at its position. */
71+
/** Allows matchers to match any distinct trace rather than the one at its position. */
6672
public static final UnaryOperator<Options> UNORDERED = Options::unorder;
6773

68-
/** Sorts traces by earliest span start time before matching. */
74+
/** Sorts traces by start time. */
6975
public static final UnaryOperator<Options> SORT_BY_START_TIME =
7076
options -> options.sort(TRACE_START_TIME_COMPARATOR);
7177

@@ -129,18 +135,21 @@ public static void assertTraces(
129135
if (skippedTraces.get(traceIndex)) {
130136
continue;
131137
}
132-
if (matcher.matches(traces.get(traceIndex).getSpans(), matcherIndex)) {
138+
try {
139+
matcher.assertTrace(traces.get(traceIndex).getSpans(), matcherIndex);
133140
matched = true;
134141
if (opts.unordered) {
135142
skippedTraces.set(traceIndex);
136143
} else {
137144
skippedTraces.set(0, traceIndex);
138145
}
139146
break;
147+
} catch (AssertionError ignored) {
148+
// Swallow assertion errors, keep looking for a match
140149
}
141150
}
142151
if (!matched) {
143-
assertionFailure().message("No trace matches matcher # "+ matcherIndex).buildAndThrow();
152+
assertionFailure().message("No trace matches matcher # " + matcherIndex).buildAndThrow();
144153
}
145154
}
146155
}

dd-smoke-tests/src/main/java/datadog/smoketest/trace/TraceMatcher.java

Lines changed: 78 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,144 @@
11
package datadog.smoketest.trace;
22

33
import static java.util.Comparator.comparingLong;
4+
import static java.util.stream.Collectors.toSet;
45

56
import datadog.trace.test.agent.decoder.DecodedSpan;
67
import java.util.ArrayList;
78
import java.util.Comparator;
89
import java.util.HashMap;
910
import java.util.List;
1011
import java.util.Map;
12+
import java.util.Set;
1113
import java.util.function.UnaryOperator;
14+
import org.opentest4j.AssertionFailedError;
1215

1316
/**
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.
1618
*
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.
1922
*
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.
2826
*
29-
* <p>A sorted order also makes {@link SpanMatcher#childOfIndex(int)} / {@link
30-
* SpanMatcher#childOfPrevious()} deterministic.
27+
* @see SmokeTraceAssertions
28+
* @see SpanMatcher
3129
*/
3230
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. */
3435
public static final Comparator<DecodedSpan> START_TIME_COMPARATOR =
35-
comparingLong(DecodedSpan::getStart);
36+
comparingLong(DecodedSpan::getStart).thenComparingLong(DecodedSpan::getSpanId);
3637

37-
/** Configures {@link #trace(UnaryOperator, SpanMatcher...)} to sort spans by start time. */
38+
/*
39+
* Span assertion options.
40+
*/
41+
/** Sorts spans by start time. */
3842
public static final UnaryOperator<Options> SORT_BY_START_TIME =
3943
options -> options.sort(START_TIME_COMPARATOR);
4044

4145
/**
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.
4648
*/
47-
public static final UnaryOperator<Options> SORT_BY_PARENT_CHAIN = Options::linearizeByParentChain;
49+
public static final UnaryOperator<Options> SORT_BY_ANCESTRY = Options::sortByAncestry;
4850

4951
private final Options options;
50-
private final SpanMatcher[] spanMatchers;
52+
private final SpanMatcher[] matchers;
5153

5254
private TraceMatcher(Options options, SpanMatcher[] spanMatchers) {
5355
if (spanMatchers.length == 0) {
5456
throw new IllegalArgumentException("No span matchers provided");
5557
}
5658
this.options = options;
57-
this.spanMatchers = spanMatchers;
59+
this.matchers = spanMatchers;
5860
}
5961

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+
*/
6167
public static TraceMatcher trace(SpanMatcher... matchers) {
6268
return new TraceMatcher(new Options(), matchers);
6369
}
6470

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+
*/
6677
public static TraceMatcher trace(UnaryOperator<Options> options, SpanMatcher... matchers) {
6778
return new TraceMatcher(options.apply(new Options()), matchers);
6879
}
6980

7081
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());
8187
}
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);
9393
}
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);
11096
}
11197
}
11298

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<>();
122102
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;
134106
}
107+
spansByParentId.computeIfAbsent(parentId, k -> new ArrayList<>()).add(span);
135108
}
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+
139111
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);
153113
return ordered;
154114
}
155115

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+
156129
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;
159132

160133
public Options sort(Comparator<DecodedSpan> comparator) {
161134
this.comparator = comparator;
135+
this.sortByAncestry = false;
162136
return this;
163137
}
164138

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;
171142
return this;
172143
}
173144
}

0 commit comments

Comments
 (0)