Skip to content

Commit cfa0b5e

Browse files
moschejackshirazi
andauthored
Fix APM tracer to respect recording status of spans (elastic#137015) (elastic#137164)
Co-authored-by: Jack Shirazi <[email protected]>
1 parent 11bd5a0 commit cfa0b5e

File tree

2 files changed

+62
-18
lines changed

2 files changed

+62
-18
lines changed

modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/tracing/APMTracer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ public void startTrace(TraceContext traceContext, Traceable traceable, String sp
188188
spanBuilder.setStartTimestamp(startTime);
189189
}
190190
final Span span = spanBuilder.startSpan();
191+
// If the agent decided not to record this span (e.g., due to transaction_max_spans), isRecording() will be false.
192+
if (span.isRecording() == false) {
193+
logger.trace("Span [{}] [{}] will not be recorded, e.g. due to transaction_max_spans reached", spanId, spanName);
194+
// It's good practice to end the no-op span immediately to release any resources.
195+
span.end();
196+
// Returning null from computeIfAbsent means no value will be inserted into the map.
197+
return null;
198+
}
199+
200+
// If we are here, the span is real and being recorded.
201+
logger.trace("Successfully started tracing [{}] [{}]", spanId, spanName);
191202
final Context contextForNewSpan = Context.current().with(span);
192203

193204
updateThreadContext(traceContext, services, contextForNewSpan);

modules/apm/src/test/java/org/elasticsearch/telemetry/apm/internal/tracing/APMTracerTests.java

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.elasticsearch.telemetry.apm.internal.APMAgentSettings;
2626
import org.elasticsearch.telemetry.tracing.Traceable;
2727
import org.elasticsearch.test.ESTestCase;
28+
import org.elasticsearch.test.junit.annotations.TestLogging;
29+
import org.mockito.Mockito;
2830

2931
import java.time.Instant;
3032
import java.util.HashMap;
@@ -44,6 +46,7 @@
4446
import static org.mockito.Mockito.doAnswer;
4547
import static org.mockito.Mockito.mock;
4648

49+
@TestLogging(reason = "improved visibility", value = "org.elasticsearch.telemetry.apm.internal.tracing:TRACE")
4750
public 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

Comments
 (0)