Skip to content

Commit 42a2a92

Browse files
authored
Avoid multiple HttpUrlConnection span ending on nested calls (#2530)
1 parent 870a4fd commit 42a2a92

File tree

3 files changed

+48
-23
lines changed

3 files changed

+48
-23
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ endif::[]
3232
* Fix logging initialization with Security Manager - {pull}2568[#2568]
3333
* normalize empty `transaction.type` and `span.type` - {pull}2525[#2525]
3434
* Allowing square brackets within the <<config-capture-jmx-metrics>> config value - {pull}2547[#2547]
35+
* Fixed duplicated ending of `HttpUrlConnection` spans - {pull}2530[#2530]
3536
3637
3738
[[release-notes-1.x]]

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import co.elastic.apm.agent.report.ReporterConfiguration;
2929
import co.elastic.apm.agent.sdk.logging.Logger;
3030
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
31+
import co.elastic.apm.agent.util.LoggerUtils;
3132

3233
import javax.annotation.Nullable;
3334
import java.util.HashMap;
@@ -44,6 +45,8 @@ public abstract class AbstractSpan<T extends AbstractSpan<T>> implements Recycla
4445
public static final int PRIO_LOW_LEVEL_FRAMEWORK = 10;
4546
public static final int PRIO_DEFAULT = 0;
4647
private static final Logger logger = LoggerFactory.getLogger(AbstractSpan.class);
48+
private static final Logger oneTimeDuplicatedEndLogger = LoggerUtils.logOnce(logger);
49+
4750
protected static final double MS_IN_MICROS = TimeUnit.MILLISECONDS.toMicros(1);
4851
protected final TraceContext traceContext;
4952

@@ -492,7 +495,12 @@ public final void end(long epochMicros) {
492495
}
493496
afterEnd();
494497
} else {
495-
logger.warn("End has already been called: {}", this);
498+
if (oneTimeDuplicatedEndLogger.isWarnEnabled()) {
499+
oneTimeDuplicatedEndLogger.warn("End has already been called: " + this, new Throwable());
500+
} else {
501+
logger.warn("End has already been called: {}", this);
502+
logger.debug("Consecutive AbstractSpan#end() invocation stack trace: ", new Throwable());
503+
}
496504
assert false;
497505
}
498506
}

apm-agent-plugins/apm-urlconnection-plugin/src/main/java/co/elastic/apm/agent/urlconnection/HttpUrlConnectionInstrumentation.java

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import co.elastic.apm.agent.impl.transaction.Outcome;
2626
import co.elastic.apm.agent.impl.transaction.Span;
2727
import co.elastic.apm.agent.impl.transaction.TraceContext;
28+
import co.elastic.apm.agent.sdk.state.CallDepth;
29+
import co.elastic.apm.agent.sdk.state.GlobalState;
2830
import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap;
2931
import net.bytebuddy.asm.Advice;
3032
import net.bytebuddy.description.NamedElement;
@@ -45,9 +47,11 @@
4547
import static net.bytebuddy.matcher.ElementMatchers.not;
4648
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
4749

50+
@GlobalState
4851
public abstract class HttpUrlConnectionInstrumentation extends TracerAwareInstrumentation {
4952

50-
private static final WeakMap<HttpURLConnection, Span> inFlightSpans = WeakConcurrentProviderImpl.createWeakSpanMap();
53+
public static final WeakMap<HttpURLConnection, Span> inFlightSpans = WeakConcurrentProviderImpl.createWeakSpanMap();
54+
public static final CallDepth callDepth = CallDepth.get(HttpUrlConnectionInstrumentation.class);
5155

5256
@Override
5357
public Collection<String> getInstrumentationGroupNames() {
@@ -73,6 +77,7 @@ public static Object enter(@Advice.This HttpURLConnection thiz,
7377
@Advice.FieldValue("connected") boolean connected,
7478
@Advice.Origin String signature) {
7579

80+
boolean isNestedCall = callDepth.isNestedCallAndIncrement();
7681
AbstractSpan<?> parent = tracer.getActive();
7782
if (parent == null) {
7883
return null;
@@ -89,10 +94,11 @@ public static Object enter(@Advice.This HttpURLConnection thiz,
8994
}
9095
}
9196
}
92-
if (span != null) {
97+
if (!isNestedCall && span != null) {
9398
span.activate();
99+
return span;
94100
}
95-
return span;
101+
return null;
96102
}
97103

98104
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
@@ -102,29 +108,39 @@ public static void exit(@Advice.This HttpURLConnection thiz,
102108
@Advice.Enter @Nullable Object spanObject,
103109
@Advice.Origin String signature) {
104110

111+
callDepth.decrement();
105112
Span span = (Span) spanObject;
106113
if (span == null) {
107114
return;
108115
}
109-
span.deactivate();
110-
if (responseCode != -1) {
111-
inFlightSpans.remove(thiz);
112-
// if the response code is set, the connection has been established via getOutputStream
113-
// if the response code is unset even after getOutputStream has been called, there will be an exception
114-
span.getContext().getHttp().withStatusCode(responseCode);
115-
span.captureException(t).end();
116-
} else if (t != null) {
117-
inFlightSpans.remove(thiz);
118-
119-
// an exception here is synonym of failure, for example with circular redirects
120-
span.captureException(t)
121-
.withOutcome(Outcome.FAILURE)
122-
.end();
123-
} else {
124-
// if connect or getOutputStream has been called we can't end the span right away
125-
// we have to store associate it with thiz HttpURLConnection instance and end once getInputStream has been called
126-
// note that this could happen on another thread
127-
inFlightSpans.put(thiz, span);
116+
try {
117+
if (responseCode != -1) {
118+
inFlightSpans.remove(thiz);
119+
// if the response code is set, the connection has been established via getOutputStream
120+
// if the response code is unset even after getOutputStream has been called, there will be an exception
121+
// checking if "finished" to avoid multiple endings on nested calls
122+
if (!span.isFinished()) {
123+
span.getContext().getHttp().withStatusCode(responseCode);
124+
span.captureException(t).end();
125+
}
126+
} else if (t != null) {
127+
inFlightSpans.remove(thiz);
128+
129+
// an exception here is synonym of failure, for example with circular redirects
130+
// checking if "finished" to avoid multiple endings on nested calls
131+
if (!span.isFinished()) {
132+
span.captureException(t)
133+
.withOutcome(Outcome.FAILURE)
134+
.end();
135+
}
136+
} else {
137+
// if connect or getOutputStream has been called we can't end the span right away
138+
// we have to store associate it with thiz HttpURLConnection instance and end once getInputStream has been called
139+
// note that this could happen on another thread
140+
inFlightSpans.put(thiz, span);
141+
}
142+
} finally {
143+
span.deactivate();
128144
}
129145
}
130146
}

0 commit comments

Comments
 (0)