Skip to content

Commit 9f05195

Browse files
committed
WIP
1 parent d8cbc05 commit 9f05195

4 files changed

Lines changed: 95 additions & 25 deletions

File tree

dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ private void assertSpanLinks(List<AgentSpanLink> links) {
451451
.expected(expectedLinkCount)
452452
.actual(linkCount)
453453
.buildAndThrow();
454+
return;
454455
}
455456
for (int i = 0; i < expectedLinkCount; i++) {
456457
SpanLinkMatcher linkMatcher = this.linkMatchers[i];

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

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.HashMap;
1414
import java.util.List;
1515
import java.util.Map;
16+
import java.util.Map.Entry;
1617
import java.util.function.Predicate;
1718
import java.util.regex.Pattern;
1819

@@ -24,7 +25,8 @@
2425
* datadog.trace.agent.test.assertions}, which matches on {@code DDSpan}); the two share only the
2526
* generic value matchers in {@code datadog.trace.junit.utils.assertions}. Only what a span carries
2627
* once serialized is matchable here: name/service/resource/type, error status, {@code meta} (string
27-
* tags), {@code metrics} (numeric tags), and parent linkage.
28+
* tags), {@code metrics} (numeric tags), {@code meta_struct} (nested structured data), and parent
29+
* linkage.
2830
*
2931
* <p>Parent linkage can be expressed by {@link #root()}, {@link #childOf(long) explicit id}, or —
3032
* for structural assertions within a sorted trace — by position with {@link #childOfIndex(int)} /
@@ -40,8 +42,10 @@ public final class SpanMatcher {
4042
private Long parentId; // explicit expected parent id (childOf/root); null => not checked by id
4143
private int parentSpanIndex = -1; // >= 0 => parent is the span at this index in the sorted trace
4244
private boolean childOfPrevious; // parent is the preceding span in the sorted trace
43-
private final Map<String, Matcher<?>> metaMatchers = new HashMap<>();
44-
private final Map<String, Matcher<?>> metricMatchers = new HashMap<>();
45+
private final Map<String, Matcher<String>> metaMatchers = new HashMap<>();
46+
private final Map<String, Matcher<Number>> metricMatchers = new HashMap<>();
47+
// meta_struct values are nested (map/list/scalar), so matched as Object.
48+
private final Map<String, Matcher<?>> metaStructMatchers = new HashMap<>();
4549

4650
private SpanMatcher() {}
4751

@@ -124,7 +128,7 @@ public SpanMatcher childOfPrevious() {
124128
}
125129

126130
/** Matches a {@code meta} (string) tag against the given matcher. */
127-
public SpanMatcher tag(String name, Matcher<?> matcher) {
131+
public SpanMatcher tag(String name, Matcher<String> matcher) {
128132
this.metaMatchers.put(name, matcher);
129133
return this;
130134
}
@@ -136,43 +140,54 @@ public SpanMatcher tag(String name, String value) {
136140
}
137141

138142
/** Matches a {@code metrics} (numeric) tag against the given matcher. */
139-
public SpanMatcher metric(String name, Matcher<?> matcher) {
143+
public SpanMatcher metric(String name, Matcher<Number> matcher) {
140144
this.metricMatchers.put(name, matcher);
141145
return this;
142146
}
143147

144-
// TODO thin: exhaustive tag coverage (fail on unexpected meta/metrics) and span-links.
148+
public SpanMatcher metaStruct(String name, Matcher<?> matcher) {
149+
this.metaStructMatchers.put(name, matcher);
150+
return this;
151+
}
145152

146-
/** Positional (count-exact) assertion: the span at {@code spanIndex} matches this matcher. */
147153
void assertSpan(List<DecodedSpan> trace, int spanIndex) {
148154
assertValue(
149155
parentIdMatcher(trace, spanIndex),
150156
trace.get(spanIndex).getParentId(),
151157
"Unexpected parent id");
152-
assertFields(trace.get(spanIndex));
153-
}
154-
155-
/** Asserts a span's own fields (service/operation/resource/type/error/tags), ignoring linkage. */
156-
@SuppressWarnings("unchecked")
157-
private void assertFields(DecodedSpan span) {
158+
DecodedSpan span = trace.get(spanIndex);
158159
assertValue(this.serviceMatcher, span.getService(), "Unexpected service name");
159160
assertValue(this.operationNameMatcher, span.getName(), "Unexpected operation name");
160161
assertValue(this.resourceNameMatcher, span.getResource(), "Unexpected resource name");
161162
assertValue(this.typeMatcher, span.getType(), "Unexpected span type");
162163
assertValue(this.errorMatcher, span.getError() != 0, "Unexpected error status");
163-
Map<String, String> meta = span.getMeta();
164-
for (Map.Entry<String, Matcher<?>> entry : this.metaMatchers.entrySet()) {
165-
Object value = meta == null ? null : meta.get(entry.getKey());
164+
assertSpanTags(span.getMeta());
165+
assertSpanMetrics(span.getMetrics());
166+
assertSpanMetaStruct(span.getMetaStruct());
167+
}
168+
169+
private void assertSpanTags(Map<String, String> meta) {
170+
for (Entry<String, Matcher<String>> entry : this.metaMatchers.entrySet()) {
171+
String key = entry.getKey();
172+
assertValue(entry.getValue(), meta.get(key), "Unexpected meta tag '" + key + "'");
173+
}
174+
}
175+
176+
private void assertSpanMetrics(Map<String, Number> metrics) {
177+
for (Entry<String, Matcher<Number>> entry : this.metricMatchers.entrySet()) {
166178
assertValue(
167-
(Matcher<Object>) entry.getValue(),
168-
value,
169-
"Unexpected meta tag '" + entry.getKey() + "'");
179+
entry.getValue(),
180+
metrics.get(entry.getKey()),
181+
"Unexpected metric '" + entry.getKey() + "'");
170182
}
171-
Map<String, Number> metrics = span.getMetrics();
172-
for (Map.Entry<String, Matcher<?>> entry : this.metricMatchers.entrySet()) {
173-
Object value = metrics == null ? null : metrics.get(entry.getKey());
183+
}
184+
185+
@SuppressWarnings("unchecked")
186+
private void assertSpanMetaStruct(Map<String, Object> metaStruct) {
187+
for (Entry<String, Matcher<?>> entry : this.metaStructMatchers.entrySet()) {
188+
String key = entry.getKey();
174189
assertValue(
175-
(Matcher<Object>) entry.getValue(), value, "Unexpected metric '" + entry.getKey() + "'");
190+
(Matcher<Object>) entry.getValue(), metaStruct.get(key), "Unexpected meta_struct '" + key + "'");
176191
}
177192
}
178193

dd-smoke-tests/src/test/java/datadog/smoketest/backend/TestAgentTraceDecoderTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import static datadog.smoketest.trace.TraceMatcher.trace;
66
import static org.junit.jupiter.api.Assertions.assertEquals;
77
import static org.junit.jupiter.api.Assertions.assertNotNull;
8-
import static org.junit.jupiter.api.Assertions.assertNull;
98
import static org.junit.jupiter.api.Assertions.assertThrows;
109
import static org.junit.jupiter.api.Assertions.assertTrue;
1110

@@ -120,7 +119,7 @@ void metaStructIsNullWhenAbsentAndAMapWhenPresent() {
120119
// Absent meta_struct decodes to null (matching the msgpack Decoder's plain-span shape).
121120
DecodedSpan withoutMetaStruct =
122121
TestAgentTraceDecoder.decode(TWO_TRACES).get(0).getSpans().get(0);
123-
assertNull(withoutMetaStruct.getMetaStruct());
122+
assertTrue(withoutMetaStruct.getMetaStruct().isEmpty());
124123

125124
String withMetaStruct =
126125
"[[{"

dd-smoke-tests/src/test/java/datadog/smoketest/trace/SmokeMatcherTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import static datadog.smoketest.trace.SpanMatcher.span;
55
import static datadog.smoketest.trace.TraceMatcher.SORT_BY_ANCESTRY;
66
import static datadog.smoketest.trace.TraceMatcher.trace;
7+
import static datadog.trace.test.junit.utils.assertions.Matchers.isNonNull;
8+
import static datadog.trace.test.junit.utils.assertions.Matchers.validates;
79
import static org.junit.jupiter.api.Assertions.assertThrows;
810

911
import datadog.smoketest.backend.TestAgentTraceDecoder;
1012
import datadog.trace.test.agent.decoder.DecodedTrace;
1113
import java.util.List;
14+
import java.util.Map;
1215
import org.junit.jupiter.api.Test;
1316

1417
/**
@@ -42,6 +45,17 @@ class SmokeMatcherTest {
4245
+ spanJson("b", 300, 100, 30)
4346
+ "]]";
4447

48+
// One span carrying meta_struct: an IAST-style nested "_dd.stack" plus a request-body entry.
49+
private static final String META_STRUCT_TRACE =
50+
"[[{"
51+
+ "\"service\":\"s\",\"name\":\"servlet.request\",\"resource\":\"r\",\"type\":\"web\","
52+
+ "\"trace_id\":1,\"span_id\":1,\"parent_id\":0,\"start\":0,\"duration\":1,\"error\":0,"
53+
+ "\"meta\":{},\"metrics\":{},"
54+
+ "\"meta_struct\":{"
55+
+ "\"_dd.stack\":{\"vulnerability\":[{\"type\":\"SQL_INJECTION\"},{\"type\":\"XSS\"}]},"
56+
+ "\"http.request.body\":{\"foo\":\"bar\"}"
57+
+ "}}]]";
58+
4559
@Test
4660
void sortsSpansByStartTimeThenMatchesParentByPrevious() {
4761
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(CHAIN_TRACE);
@@ -144,6 +158,47 @@ void unorderedRequiresDistinctTraces() {
144158
trace(span().operationName("root-a").root())));
145159
}
146160

161+
@Test
162+
void matchesMetaStructByMatcherAndPredicate() {
163+
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(META_STRUCT_TRACE);
164+
assertTraces(
165+
traces,
166+
trace(
167+
span()
168+
.operationName("servlet.request")
169+
.root()
170+
// plain matcher: the entry is present (any nested value)
171+
.metaStruct("http.request.body", isNonNull())
172+
// predicate via validates(): navigate the nested structure
173+
.metaStruct(
174+
"_dd.stack",
175+
validates(v -> ((List<?>) ((Map<?, ?>) v).get("vulnerability")).size() == 2))));
176+
}
177+
178+
@Test
179+
void metaStructMismatchFails() {
180+
List<DecodedTrace> traces = TestAgentTraceDecoder.decode(META_STRUCT_TRACE);
181+
// Absent entry -> null value -> matcher fails.
182+
assertThrows(
183+
AssertionError.class,
184+
() -> assertTraces(traces, trace(span().root().metaStruct("does.not.exist", isNonNull()))));
185+
// Present entry but the predicate over its nested value is not satisfied.
186+
assertThrows(
187+
AssertionError.class,
188+
() ->
189+
assertTraces(
190+
traces,
191+
trace(
192+
span()
193+
.root()
194+
.metaStruct(
195+
"_dd.stack",
196+
validates(
197+
v ->
198+
((List<?>) ((Map<?, ?>) v).get("vulnerability")).size()
199+
== 99)))));
200+
}
201+
147202
private static String spanJson(String name, long id, long parent, long start) {
148203
return "{\"service\":\"s\",\"name\":\""
149204
+ name

0 commit comments

Comments
 (0)