2525import org .elasticsearch .telemetry .apm .internal .APMAgentSettings ;
2626import org .elasticsearch .telemetry .tracing .Traceable ;
2727import org .elasticsearch .test .ESTestCase ;
28+ import org .elasticsearch .test .junit .annotations .TestLogging ;
29+ import org .mockito .Mockito ;
2830
2931import java .time .Instant ;
3032import java .util .HashMap ;
4446import static org .mockito .Mockito .doAnswer ;
4547import static org .mockito .Mockito .mock ;
4648
49+ @ TestLogging (reason = "improved visibility" , value = "org.elasticsearch.telemetry.apm.internal.tracing:TRACE" )
4750public class APMTracerTests extends ESTestCase {
4851
4952 private static final Traceable TRACEABLE1 = new TestTraceable ("id1" );
@@ -90,6 +93,37 @@ public void test_onTraceStarted_startsTrace() {
9093 assertThat (apmTracer .getSpans (), hasKey (TRACEABLE1 .getSpanId ()));
9194 }
9295
96+ /**
97+ * Check that when a trace is started, but it is not recorded, e.g. due to sampling, the tracer does not record it either.
98+ */
99+ public void test_onTraceStarted_ifNotRecorded_doesNotStartTrace () {
100+ Settings settings = Settings .builder ().put (APMAgentSettings .TELEMETRY_TRACING_ENABLED_SETTING .getKey (), true ).build ();
101+ APMTracer apmTracer = buildTracer (settings );
102+
103+ apmTracer .startTrace (new ThreadContext (settings ), TRACEABLE1 , "name1_discard" , null );
104+
105+ assertThat (apmTracer .getSpans (), anEmptyMap ());
106+ }
107+
108+ /**
109+ * Check that when a nested trace is discarded e.g.g due to transaction_max_spans exceeded, the tracer does not record it.
110+ */
111+ public void test_onNestedTraceStarted_ifNotRecorded_doesNotStartTrace () {
112+ Settings settings = Settings .builder ().put (APMAgentSettings .TELEMETRY_TRACING_ENABLED_SETTING .getKey (), true ).build ();
113+ APMTracer apmTracer = buildTracer (settings );
114+
115+ ThreadContext traceContext = new ThreadContext (settings );
116+ apmTracer .startTrace (traceContext , TRACEABLE1 , "name1" , null );
117+ try (var ignore1 = traceContext .newTraceContext ()) {
118+ apmTracer .startTrace (traceContext , TRACEABLE2 , "name2_discard" , null );
119+ try (var ignore2 = traceContext .newTraceContext ()) {
120+ apmTracer .startTrace (traceContext , TRACEABLE3 , "name3_discard" , null );
121+ }
122+ }
123+ assertThat (apmTracer .getSpans (), aMapWithSize (1 ));
124+ assertThat (apmTracer .getSpans (), hasKey (TRACEABLE1 .getSpanId ()));
125+ }
126+
93127 /**
94128 * Checks that when a trace is started with a specific start time, the tracer starts a span and records it.
95129 */
@@ -266,8 +300,7 @@ APMServices createApmServices() {
266300 Tracer mockTracer = mock (Tracer .class );
267301 doAnswer (invocation -> {
268302 String spanName = (String ) invocation .getArguments ()[0 ];
269- // spy the spanBuilder
270- return new SpySpanBuilder (apmServices .tracer (), spanName );
303+ return new MockSpanBuilder (spanName );
271304 }).when (mockTracer ).spanBuilder (anyString ());
272305 return new APMServices (mockTracer , apmServices .openTelemetry ());
273306 }
@@ -276,89 +309,89 @@ Instant getSpanStartTime(String spanName) {
276309 return spanStartTimeMap .get (spanName );
277310 }
278311
279- class SpySpanBuilder implements SpanBuilder {
312+ /**
313+ * There's no APM agent in unit tests. Spans created by the default span builder would be NOOP spans that are not recorded.
314+ * This builder simulates recorded spans so that we can test the tracer behavior.
315+ */
316+ class MockSpanBuilder implements SpanBuilder {
280317
281- SpanBuilder delegatedSpanBuilder ;
318+ Span span ;
282319 Instant startTime ;
283320 String spanName ;
284321
285- SpySpanBuilder (Tracer tracer , String spanName ) {
286- this .delegatedSpanBuilder = tracer .spanBuilder (spanName );
322+ MockSpanBuilder (String spanName ) {
287323 this .spanName = spanName ;
324+ this .span = Mockito .mock (Span .class , spanName );
325+ // simulate discarded span due to transaction_max_spans exceeded
326+ Mockito .when (span .isRecording ()).thenReturn (spanName .endsWith ("_discard" ) == false );
327+ Mockito .when (span .storeInContext (Mockito .any (Context .class ))).thenCallRealMethod ();
288328 }
289329
290330 @ Override
291331 public SpanBuilder setParent (Context context ) {
292- delegatedSpanBuilder .setParent (context );
332+ SpanContext spanContext = Span .fromContext (context ).getSpanContext ();
333+ Mockito .when (span .getSpanContext ()).thenReturn (spanContext );
293334 return this ;
294335 }
295336
296337 @ Override
297338 public SpanBuilder setNoParent () {
298- delegatedSpanBuilder .setNoParent ();
339+ SpanContext invalid = SpanContext .getInvalid ();
340+ Mockito .when (span .getSpanContext ()).thenReturn (invalid );
299341 return this ;
300342 }
301343
302344 @ Override
303345 public SpanBuilder addLink (SpanContext spanContext ) {
304- delegatedSpanBuilder .addLink (spanContext );
305346 return this ;
306347 }
307348
308349 @ Override
309350 public SpanBuilder addLink (SpanContext spanContext , Attributes attributes ) {
310- delegatedSpanBuilder .addLink (spanContext , attributes );
311351 return this ;
312352 }
313353
314354 @ Override
315355 public SpanBuilder setAttribute (String key , String value ) {
316- delegatedSpanBuilder .setAttribute (key , value );
317356 return this ;
318357 }
319358
320359 @ Override
321360 public SpanBuilder setAttribute (String key , long value ) {
322- delegatedSpanBuilder .setAttribute (key , value );
323361 return this ;
324362 }
325363
326364 @ Override
327365 public SpanBuilder setAttribute (String key , double value ) {
328- delegatedSpanBuilder .setAttribute (key , value );
329366 return this ;
330367 }
331368
332369 @ Override
333370 public SpanBuilder setAttribute (String key , boolean value ) {
334- delegatedSpanBuilder .setAttribute (key , value );
335371 return this ;
336372 }
337373
338374 @ Override
339375 public <T > SpanBuilder setAttribute (AttributeKey <T > key , T value ) {
340- delegatedSpanBuilder .setAttribute (key , value );
341376 return this ;
342377 }
343378
344379 @ Override
345380 public SpanBuilder setSpanKind (SpanKind spanKind ) {
346- delegatedSpanBuilder .setSpanKind (spanKind );
347381 return this ;
348382 }
349383
350384 @ Override
351385 public SpanBuilder setStartTimestamp (long startTimestamp , TimeUnit unit ) {
352386 startTime = Instant .ofEpochMilli (TimeUnit .MILLISECONDS .convert (startTimestamp , unit ));
353- delegatedSpanBuilder .setStartTimestamp (startTimestamp , unit );
354387 return this ;
355388 }
356389
357390 @ Override
358391 public Span startSpan () {
359392 // finally record the spanName-startTime association when the span is actually started
360393 spanStartTimeMap .put (spanName , startTime );
361- return delegatedSpanBuilder . startSpan () ;
394+ return span ;
362395 }
363396 }
364397 }
0 commit comments