Skip to content

Commit 06bcfd3

Browse files
Add span event assertions to smoke tests (#12934)
feat(testing): Add span event decoding feat(testing): Add span event smoke test API feat(otel): Improve span event testing Co-authored-by: bruce.bujon <bruce.bujon@datadoghq.com>
1 parent e1b76db commit 06bcfd3

18 files changed

Lines changed: 1212 additions & 18 deletions

File tree

‎dd-smoke-tests/opentelemetry/src/test/java/datadog/smoketest/OpenTelemetrySmokeTest.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static datadog.smoketest.backend.AgentBackend.testAgent;
44
import static datadog.smoketest.trace.SmokeTraceAssertions.UNORDERED;
5+
import static datadog.smoketest.trace.SpanEventMatcher.event;
56
import static datadog.smoketest.trace.SpanLinkMatcher.toIndex;
67
import static datadog.smoketest.trace.SpanMatcher.span;
78
import static datadog.smoketest.trace.TraceMatcher.trace;
@@ -46,7 +47,7 @@ void receivesTraces() {
4647
span()
4748
.root()
4849
.operationName("Application.loadConfiguration")
49-
.tag("events", validates(events -> events.contains("configuration-loaded")))),
50+
.events(event("configuration-loaded"))),
5051
// The batch trace:
5152
// batch-job (tracer, server, root)
5253
// ├─ Application.splitBatch (annotated, main thread)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package datadog.smoketest.trace;
2+
3+
import static datadog.trace.test.junit.utils.assertions.Matchers.assertValue;
4+
import static datadog.trace.test.junit.utils.assertions.Matchers.is;
5+
import static datadog.trace.test.junit.utils.assertions.Matchers.validates;
6+
import static java.util.Collections.emptyMap;
7+
8+
import datadog.trace.test.agent.decoder.DecodedSpan;
9+
import datadog.trace.test.agent.decoder.DecodedSpanEvent;
10+
import datadog.trace.test.junit.utils.assertions.Matcher;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.concurrent.TimeUnit;
14+
15+
/**
16+
* Provides matchers for decoded span events based on their properties such as name, time, and
17+
* attributes.
18+
*/
19+
public final class SpanEventMatcher {
20+
private final Matcher<String> nameMatcher;
21+
private Matcher<Long> timeMatcher;
22+
private Matcher<Map<String, Object>> attributesMatcher;
23+
24+
private SpanEventMatcher(Matcher<String> nameMatcher) {
25+
this.nameMatcher = nameMatcher;
26+
this.attributesMatcher = is(emptyMap());
27+
}
28+
29+
/**
30+
* Creates a {@code SpanEventMatcher} that matches a span event with the given name. By default,
31+
* the event must have no attribute and its time must be within the span start and end times.
32+
*
33+
* @param name The span event name to match against.
34+
* @return A {@code SpanEventMatcher} that matches a span event with the given name.
35+
*/
36+
public static SpanEventMatcher event(String name) {
37+
return new SpanEventMatcher(is(name));
38+
}
39+
40+
/**
41+
* Checks the event time matches the given value.
42+
*
43+
* @param timestamp The event time to match against.
44+
* @param unit The event time unit.
45+
* @return The current {@link SpanEventMatcher} instance with the time constraint applied.
46+
*/
47+
public SpanEventMatcher time(long timestamp, TimeUnit unit) {
48+
this.timeMatcher = is(unit.toNanos(timestamp));
49+
return this;
50+
}
51+
52+
/**
53+
* Checks the event time matches the given matcher.
54+
*
55+
* @param matcher The matcher to check the event time, in nanoseconds since the Unix epoch.
56+
* @return The current {@link SpanEventMatcher} instance with the time constraint applied.
57+
*/
58+
public SpanEventMatcher time(Matcher<Long> matcher) {
59+
this.timeMatcher = matcher;
60+
return this;
61+
}
62+
63+
/**
64+
* Checks the event attributes match the given values.
65+
*
66+
* @param attributes The event attributes to match against, with values being {@link String},
67+
* {@link Boolean}, {@link Long}, {@link Double}, or a {@link List} of them.
68+
* @return The current {@link SpanEventMatcher} instance with the attributes constraint applied.
69+
*/
70+
public SpanEventMatcher attributes(Map<String, Object> attributes) {
71+
this.attributesMatcher = is(attributes);
72+
return this;
73+
}
74+
75+
/**
76+
* Checks the event attributes match the given matcher.
77+
*
78+
* @param matcher The matcher to check the event attributes.
79+
* @return The current {@link SpanEventMatcher} instance with the attributes constraint applied.
80+
*/
81+
public SpanEventMatcher attributes(Matcher<Map<String, Object>> matcher) {
82+
this.attributesMatcher = matcher;
83+
return this;
84+
}
85+
86+
void assertEvent(DecodedSpan span, DecodedSpanEvent event, int eventIndex) {
87+
String at = " (event #" + eventIndex + ")";
88+
Matcher<Long> timeMatcher = this.timeMatcher;
89+
if (timeMatcher == null) {
90+
long startTime = span.getStart();
91+
long endTime = startTime + span.getDuration();
92+
timeMatcher = validates(time -> startTime <= time && time <= endTime);
93+
}
94+
assertValue(this.nameMatcher, event.getName(), "Unexpected span event name" + at);
95+
assertValue(timeMatcher, event.getTimeUnixNano(), "Unexpected span event time" + at);
96+
assertValue(
97+
this.attributesMatcher, event.getAttributes(), "Unexpected span event attributes" + at);
98+
}
99+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure;
1010

1111
import datadog.trace.test.agent.decoder.DecodedSpan;
12+
import datadog.trace.test.agent.decoder.DecodedSpanEvent;
1213
import datadog.trace.test.agent.decoder.DecodedSpanLink;
1314
import datadog.trace.test.junit.utils.assertions.Matcher;
1415
import java.util.HashMap;
@@ -40,6 +41,7 @@
4041
* <li>span metrics (numeric) tags with {@link #metric(String, Matcher)}
4142
* <li>span meta_struct (nested structured data) with {@link #metaStruct(String, Matcher)}
4243
* <li>span links with {@link #links(SpanLinkMatcher...)}
44+
* <li>span events with {@link #events(SpanEventMatcher...)}
4345
* </ul>
4446
*/
4547
public final class SpanMatcher {
@@ -51,6 +53,7 @@ public final class SpanMatcher {
5153
private Matcher<Long> parentIdMatcher;
5254
private int parentSpanIndex;
5355
private SpanLinkMatcher[] linkMatchers;
56+
private SpanEventMatcher[] eventMatchers;
5457
private final Map<String, Matcher<String>> metaMatchers;
5558
private final Map<String, Matcher<Number>> metricMatchers;
5659
private final Map<String, Matcher<?>> metaStructMatchers;
@@ -234,6 +237,20 @@ public SpanMatcher links(SpanLinkMatcher... matchers) {
234237
return this;
235238
}
236239

240+
/**
241+
* Checks the span events, one {@link SpanEventMatcher} per expected event, matched positionally
242+
* and count-exact. Call it with no argument to assert the span carries no event at all; not
243+
* calling it asserts nothing about events.
244+
*
245+
* @param matchers The matchers to verify the span events, one per expected event.
246+
* @return The current {@link SpanMatcher} instance updated with the specified span event
247+
* constraints.
248+
*/
249+
public SpanMatcher events(SpanEventMatcher... matchers) {
250+
this.eventMatchers = matchers;
251+
return this;
252+
}
253+
237254
/**
238255
* Checks the span meta (string) tag matches the given matcher.
239256
*
@@ -298,6 +315,7 @@ void assertSpan(List<DecodedSpan> trace, int spanIndex) {
298315
assertSpanMetrics(span.getMetrics());
299316
assertSpanMetaStruct(span.getMetaStruct());
300317
assertSpanLinks(trace, span);
318+
assertSpanEvents(span);
301319
}
302320

303321
private Matcher<Long> parentIdMatcher(List<DecodedSpan> trace, int spanIndex) {
@@ -330,6 +348,23 @@ private void assertSpanLinks(List<DecodedSpan> trace, DecodedSpan span) {
330348
}
331349
}
332350

351+
private void assertSpanEvents(DecodedSpan span) {
352+
if (this.eventMatchers == null) {
353+
return;
354+
}
355+
List<DecodedSpanEvent> events = span.getEvents();
356+
if (events.size() != this.eventMatchers.length) {
357+
assertionFailure()
358+
.message("Unexpected span event count")
359+
.expected(this.eventMatchers.length)
360+
.actual(events.size())
361+
.buildAndThrow();
362+
}
363+
for (int i = 0; i < this.eventMatchers.length; i++) {
364+
this.eventMatchers[i].assertEvent(span, events.get(i), i);
365+
}
366+
}
367+
333368
private void assertSpanTags(Map<String, String> meta) {
334369
for (Entry<String, Matcher<String>> entry : this.metaMatchers.entrySet()) {
335370
String key = entry.getKey();

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

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static datadog.smoketest.trace.SmokeTraceAssertions.IGNORE_ADDITIONAL_TRACES;
44
import static datadog.smoketest.trace.SmokeTraceAssertions.assertTraces;
5+
import static datadog.smoketest.trace.SpanEventMatcher.event;
56
import static datadog.smoketest.trace.SpanLinkMatcher.any;
67
import static datadog.smoketest.trace.SpanLinkMatcher.to;
78
import static datadog.smoketest.trace.SpanLinkMatcher.toIndex;
@@ -12,10 +13,12 @@
1213
import static datadog.trace.test.junit.utils.assertions.Matchers.isNonNull;
1314
import static datadog.trace.test.junit.utils.assertions.Matchers.validates;
1415
import static java.util.Collections.singletonMap;
16+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
1517
import static org.junit.jupiter.api.Assertions.assertThrows;
1618

1719
import datadog.trace.test.agent.decoder.DecodedTrace;
1820
import datadog.trace.test.agent.decoder.Decoder;
21+
import java.util.LinkedHashMap;
1922
import java.util.List;
2023
import java.util.Map;
2124
import org.junit.jupiter.api.Test;
@@ -54,7 +57,7 @@ class SmokeMatcherTest {
5457
// One span carrying meta_struct: an IAST-style nested "_dd.stack" plus a request-body entry.
5558
private static final String META_STRUCT_TRACE =
5659
"[[{"
57-
+ "\"service\":\"s\",\"name\":\"servlet.request\",\"resource\":\"r\","
60+
+ "\"service\":\"test-service\",\"name\":\"servlet.request\",\"resource\":\"r\","
5861
+ "\"trace_id\":1,\"span_id\":1,\"parent_id\":0,\"start\":0,\"duration\":1,\"error\":0,"
5962
+ "\"meta\":{},\"metrics\":{},"
6063
+ "\"meta_struct\":{"
@@ -242,12 +245,14 @@ void defaultConstraintsRequireDefinedServiceNoTypeAndNoError() {
242245
trace(span().root()));
243246

244247
// A non-null span type fails the default unless acknowledged with type(...).
245-
List<DecodedTrace> typed = Decoder.decodeJson(spanTraceJson("s", "web", 0)).getTraces();
248+
List<DecodedTrace> typed =
249+
Decoder.decodeJson(spanTraceJson("test-service", "web", 0)).getTraces();
246250
assertThrows(AssertionError.class, () -> assertTraces(typed, trace(span().root())));
247251
assertTraces(typed, trace(span().root().type("web")));
248252

249253
// An errored span fails the default unless acknowledged with error(true).
250-
List<DecodedTrace> errored = Decoder.decodeJson(spanTraceJson("s", null, 1)).getTraces();
254+
List<DecodedTrace> errored =
255+
Decoder.decodeJson(spanTraceJson("test-service", null, 1)).getTraces();
251256
assertThrows(AssertionError.class, () -> assertTraces(errored, trace(span().root())));
252257
assertTraces(errored, trace(span().root().error(true)));
253258

@@ -410,6 +415,77 @@ void linkTargetsDoNotLeakAcrossCandidates() {
410415
span().operationName("root").root(), span().operationName("merge").links(toIndex(0))));
411416
}
412417

418+
// --- Span events ---
419+
420+
@Test
421+
void matchesEventsPositionally() {
422+
List<DecodedTrace> traces =
423+
Decoder.decodeJson(
424+
eventTraceJson(
425+
"[{\"time_unix_nano\":110,\"name\":\"first\"},"
426+
+ "{\"time_unix_nano\":120,\"name\":\"second\","
427+
+ "\"attributes\":{\"key\":\"value\",\"count\":3}}]"))
428+
.getTraces();
429+
Map<String, Object> attributes = new LinkedHashMap<>();
430+
attributes.put("key", "value");
431+
attributes.put("count", 3L);
432+
433+
assertTraces(
434+
traces,
435+
trace(
436+
span()
437+
.operationName("event")
438+
.events(event("first"), event("second").attributes(attributes))));
439+
assertThrows(
440+
AssertionError.class,
441+
() -> assertTraces(traces, trace(span().events(event("second"), event("first")))),
442+
"events are matched positionally");
443+
assertThrows(
444+
AssertionError.class,
445+
() -> assertTraces(traces, trace(span().events(event("first"), event("second")))),
446+
"missing attributes");
447+
}
448+
449+
@Test
450+
void eventTimeDefaultsToSpanBounds() {
451+
List<DecodedTrace> traces =
452+
Decoder.decodeJson(eventTraceJson("[{\"time_unix_nano\":42,\"name\":\"early\"}]"))
453+
.getTraces();
454+
455+
assertThrows(
456+
AssertionError.class,
457+
() -> assertTraces(traces, trace(span().events(event("early")))),
458+
"event time outside the span bounds");
459+
assertTraces(traces, trace(span().events(event("early").time(42, NANOSECONDS))));
460+
}
461+
462+
@Test
463+
void emptyEventsAssertsNoEventAndAbsentEventsAssertNothing() {
464+
List<DecodedTrace> withEvents =
465+
Decoder.decodeJson(eventTraceJson("[{\"time_unix_nano\":110,\"name\":\"event\"}]"))
466+
.getTraces();
467+
468+
assertThrows(
469+
AssertionError.class,
470+
() -> assertTraces(withEvents, trace(span().events())),
471+
"events() requires zero events");
472+
// Not calling events(...) asserts nothing, even though the span does carry events.
473+
assertTraces(withEvents, trace(span().operationName("event")));
474+
// A span with no event satisfies events().
475+
List<DecodedTrace> withoutEvents =
476+
Decoder.decodeJson("[[" + spanJson("plain", 10, 0, 10) + "]]").getTraces();
477+
assertTraces(withoutEvents, trace(span().operationName("plain").events()));
478+
}
479+
480+
/** A single-span trace, from 100 to 200 ns, carrying the given events meta tag. */
481+
private static String eventTraceJson(String eventsTag) {
482+
return "[[{\"service\":\"test-service\",\"name\":\"event\",\"resource\":\"event\","
483+
+ "\"trace_id\":1,\"span_id\":1,\"parent_id\":0,\"start\":100,\"duration\":100,"
484+
+ "\"error\":0,\"metrics\":{},\"meta\":{\"events\":\""
485+
+ eventsTag.replace("\"", "\\\"")
486+
+ "\"}}]]";
487+
}
488+
413489
/** A single-span trace whose root {@code merge} span links to the given span ids. */
414490
private static String mergeOnlyTraceJson(long... linkedSpanIds) {
415491
return "[[" + linkingSpanJson(40, 0, linkedSpanIds) + "]]";
@@ -430,7 +506,7 @@ private static String linkedTraceJson(long shardA, long shardB) {
430506

431507
/** A single-span trace whose link carries flags, a trace state and attributes. */
432508
private static String refinedLinkTraceJson() {
433-
return "[[{\"service\":\"s\",\"name\":\"merge\",\"resource\":\"merge\","
509+
return "[[{\"service\":\"test-service\",\"name\":\"merge\",\"resource\":\"merge\","
434510
+ "\"trace_id\":1,\"span_id\":40,\"parent_id\":0,\"start\":40,\"duration\":1,"
435511
+ "\"error\":0,\"metrics\":{},\"meta\":{\"_dd.span_links\":\""
436512
+ "[{\\\"trace_id\\\":\\\"00000000000000000000000000000063\\\","
@@ -455,7 +531,7 @@ private static String linkingSpanJson(long id, long parent, long... linkedSpanId
455531
.append("\\\"}");
456532
}
457533
links.append(']');
458-
return "{\"service\":\"s\",\"name\":\"merge\",\"resource\":\"merge\","
534+
return "{\"service\":\"test-service\",\"name\":\"merge\",\"resource\":\"merge\","
459535
+ "\"trace_id\":1,\"span_id\":"
460536
+ id
461537
+ ",\"parent_id\":"
@@ -467,7 +543,7 @@ private static String linkingSpanJson(long id, long parent, long... linkedSpanId
467543
}
468544

469545
private static String spanJson(String name, long id, long parent, long start) {
470-
return "{\"service\":\"s\",\"name\":\""
546+
return "{\"service\":\"test-service\",\"name\":\""
471547
+ name
472548
+ "\",\"resource\":\""
473549
+ name

‎utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/DecodedSpan.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ public interface DecodedSpan {
4141
* @return The span links, empty when the span carries none.
4242
*/
4343
List<DecodedSpanLink> getLinks();
44+
45+
/**
46+
* Returns the events.
47+
*
48+
* @return The span events, empty when the span carries none.
49+
*/
50+
List<DecodedSpanEvent> getEvents();
4451
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package datadog.trace.test.agent.decoder;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
/**
7+
* This interface describes an event a decoded span carries, as the tracer serialized it. It is the
8+
* wire-side counterpart of the tracer's {@code DDSpanEvent}.
9+
*
10+
* <p>Every accessor is non-null: absent attributes decode to an empty map.
11+
*/
12+
public interface DecodedSpanEvent {
13+
/**
14+
* Returns the event name.
15+
*
16+
* @return The event name.
17+
*/
18+
String getName();
19+
20+
/**
21+
* Returns the event time.
22+
*
23+
* @return The event time, in nanoseconds since the Unix epoch.
24+
*/
25+
long getTimeUnixNano();
26+
27+
/**
28+
* Returns the attributes attached to this event.
29+
*
30+
* @return The event attributes, with values being {@link String}, {@link Boolean}, {@link Long},
31+
* {@link Double}, or a {@link List} of them; empty if the tracer sent none.
32+
*/
33+
Map<String, Object> getAttributes();
34+
}

0 commit comments

Comments
 (0)