Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.HttpUrlConnectionSingletons.HTTP_URL_STATE;
import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.HttpUrlConnectionSingletons.instrumenter;
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
Expand All @@ -17,11 +18,11 @@

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.net.HttpURLConnection;
import javax.annotation.Nullable;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
Expand Down Expand Up @@ -57,98 +58,110 @@ public void transform(TypeTransformer transformer) {
@SuppressWarnings("unused")
public static class HttpUrlConnectionAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.This HttpURLConnection connection,
@Advice.FieldValue("connected") boolean connected,
@Advice.Local("otelHttpUrlState") HttpUrlState httpUrlState,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") CallDepth callDepth) {

callDepth = CallDepth.forClass(HttpURLConnection.class);
if (callDepth.getAndIncrement() > 0) {
// only want the rest of the instrumentation rules (which are complex enough) to apply to
// top-level HttpURLConnection calls
return;
public static class AdviceScope {
private final CallDepth callDepth;
private final HttpUrlState httpUrlState;
private final Scope scope;

private AdviceScope(CallDepth callDepth, HttpUrlState httpUrlState, Scope scope) {
this.callDepth = callDepth;
this.httpUrlState = httpUrlState;
this.scope = scope;
}

Context parentContext = currentContext();
if (!instrumenter().shouldStart(parentContext, connection)) {
return;
public static AdviceScope start(CallDepth callDepth, HttpURLConnection connection) {
if (callDepth.getAndIncrement() > 0) {
// only want the rest of the instrumentation rules (which are complex enough) to apply to
// top-level HttpURLConnection calls
return new AdviceScope(callDepth, null, null);
}

Context parentContext = currentContext();
if (!instrumenter().shouldStart(parentContext, connection)) {
return new AdviceScope(callDepth, null, null);
}

// using virtual field for a couple of reasons:
// - to start an operation in connect() and end it in getInputStream()
// - to avoid creating a new operation on multiple subsequent calls to getInputStream()
HttpUrlState httpUrlState = HTTP_URL_STATE.get(connection);

if (httpUrlState != null) {
if (!httpUrlState.finished) {
return new AdviceScope(callDepth, httpUrlState, httpUrlState.context.makeCurrent());
}
return new AdviceScope(callDepth, httpUrlState, null);
}

Context context = instrumenter().start(parentContext, connection);
httpUrlState = new HttpUrlState(context);
HTTP_URL_STATE.set(connection, httpUrlState);
return new AdviceScope(callDepth, httpUrlState, context.makeCurrent());
}

// using storage for a couple of reasons:
// - to start an operation in connect() and end it in getInputStream()
// - to avoid creating a new operation on multiple subsequent calls to getInputStream()
VirtualField<HttpURLConnection, HttpUrlState> storage =
VirtualField.find(HttpURLConnection.class, HttpUrlState.class);
httpUrlState = storage.get(connection);
public void end(
HttpURLConnection connection,
int responseCode,
@Nullable Throwable throwable,
String methodName) {
if (callDepth.decrementAndGet() > 0 || scope == null) {
return;
}

if (httpUrlState != null) {
if (!httpUrlState.finished) {
scope = httpUrlState.context.makeCurrent();
// prevent infinite recursion in case end() captures response headers due to
// HttpUrlConnection.getHeaderField() calling HttpUrlConnection.getInputStream() which then
// enters this advice again
callDepth.getAndIncrement();
try {
scope.close();
Class<? extends HttpURLConnection> connectionClass = connection.getClass();

String requestMethod = connection.getRequestMethod();
GetOutputStreamContext.set(
httpUrlState.context, connectionClass, methodName, requestMethod);

if (throwable != null) {
if (responseCode >= 400) {
// HttpURLConnection unnecessarily throws exception on error response.
// None of the other http clients do this, so not recording the exception on the span
// to be consistent with the telemetry for other http clients.
instrumenter().end(httpUrlState.context, connection, responseCode, null);
} else {
instrumenter()
.end(
httpUrlState.context,
connection,
responseCode > 0 ? responseCode : httpUrlState.statusCode,
throwable);
}
httpUrlState.finished = true;
} else if (methodName.equals("getInputStream") && responseCode > 0) {
// responseCode field is sometimes not populated.
// We can't call getResponseCode() due to some unwanted side-effects
// (e.g. breaks getOutputStream).
instrumenter().end(httpUrlState.context, connection, responseCode, null);
httpUrlState.finished = true;
}
} finally {
callDepth.decrementAndGet();
}
return;
}
}

Context context = instrumenter().start(parentContext, connection);
httpUrlState = new HttpUrlState(context);
storage.set(connection, httpUrlState);
scope = context.makeCurrent();
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AdviceScope methodEnter(@Advice.This HttpURLConnection connection) {
CallDepth callDepth = CallDepth.forClass(HttpURLConnection.class);
return AdviceScope.start(callDepth, connection);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.This HttpURLConnection connection,
@Advice.FieldValue("responseCode") int responseCode,
@Advice.Thrown Throwable throwable,
@Advice.Thrown @Nullable Throwable throwable,
@Advice.Origin("#m") String methodName,
@Advice.Local("otelHttpUrlState") HttpUrlState httpUrlState,
@Advice.Local("otelScope") Scope scope,
@Advice.Local("otelCallDepth") CallDepth callDepth) {
if (callDepth.decrementAndGet() > 0) {
return;
}
if (scope == null) {
return;
}
// prevent infinite recursion in case end() captures response headers due to
// HttpUrlConnection.getHeaderField() calling HttpUrlConnection.getInputStream() which then
// enters this advice again
callDepth.getAndIncrement();
try {
scope.close();
Class<? extends HttpURLConnection> connectionClass = connection.getClass();

String requestMethod = connection.getRequestMethod();
GetOutputStreamContext.set(
httpUrlState.context, connectionClass, methodName, requestMethod);

if (throwable != null) {
if (responseCode >= 400) {
// HttpURLConnection unnecessarily throws exception on error response.
// None of the other http clients do this, so not recording the exception on the span
// to be consistent with the telemetry for other http clients.
instrumenter().end(httpUrlState.context, connection, responseCode, null);
} else {
instrumenter()
.end(
httpUrlState.context,
connection,
responseCode > 0 ? responseCode : httpUrlState.statusCode,
throwable);
}
httpUrlState.finished = true;
} else if (methodName.equals("getInputStream") && responseCode > 0) {
// responseCode field is sometimes not populated.
// We can't call getResponseCode() due to some unwanted side-effects
// (e.g. breaks getOutputStream).
instrumenter().end(httpUrlState.context, connection, responseCode, null);
httpUrlState.finished = true;
}
} finally {
callDepth.decrementAndGet();
}
@Advice.Enter AdviceScope adviceScope) {
adviceScope.end(connection, responseCode, throwable, methodName);
}
}

Expand All @@ -159,9 +172,7 @@ public static class GetResponseCodeAdvice {
public static void methodExit(
@Advice.This HttpURLConnection connection, @Advice.Return int returnValue) {

VirtualField<HttpURLConnection, HttpUrlState> storage =
VirtualField.find(HttpURLConnection.class, HttpUrlState.class);
HttpUrlState httpUrlState = storage.get(connection);
HttpUrlState httpUrlState = HTTP_URL_STATE.get(connection);
if (httpUrlState != null) {
httpUrlState.statusCode = returnValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class HttpUrlConnectionInstrumentationModule extends InstrumentationModule {
public class HttpUrlConnectionInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {

public HttpUrlConnectionInstrumentationModule() {
super("http-url-connection");
Expand All @@ -23,4 +25,9 @@ public HttpUrlConnectionInstrumentationModule() {
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpUrlConnectionInstrumentation());
}

@Override
public boolean isIndyReady() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
package io.opentelemetry.javaagent.instrumentation.httpurlconnection;

import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpClientInstrumenters;
import java.net.HttpURLConnection;

public final class HttpUrlConnectionSingletons {

public static final VirtualField<HttpURLConnection, HttpUrlState> HTTP_URL_STATE =
VirtualField.find(HttpURLConnection.class, HttpUrlState.class);

private static final Instrumenter<HttpURLConnection, Integer> INSTRUMENTER;

static {
Expand Down
Loading