Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate span for input and output streams. #425

Merged
merged 2 commits into from
Aug 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ public static ResettableClassFileTransformer installBytebuddyAgent(
.or(named("java.net.HttpURLConnection"))
.or(nameStartsWith("java.util.concurrent.")))))
.or(nameStartsWith("com.sun."))
.or(nameStartsWith("sun.").and(not(nameStartsWith("sun.net.www.protocol."))))
.or(
nameStartsWith("sun.")
.and(
not(
nameStartsWith("sun.net.www.protocol.")
.or(named("sun.net.www.http.HttpClient")))))
.or(nameStartsWith("jdk."))
.or(nameStartsWith("org.aspectj."))
.or(nameStartsWith("org.groovy."))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import static io.opentracing.log.Fields.ERROR_OBJECT;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;

Expand All @@ -16,13 +15,11 @@
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import javax.net.ssl.HttpsURLConnection;
Expand Down Expand Up @@ -50,26 +47,16 @@ public ElementMatcher typeMatcher() {

@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.instrumentation.http_url_connection.MessageHeadersInjectAdapter",
HttpUrlConnectionInstrumentation.class.getName() + "$HttpURLState"
};
return new String[] {HttpUrlConnectionInstrumentation.class.getName() + "$HttpURLState"};
}

@Override
public Map<ElementMatcher, String> transformers() {
final Map<ElementMatcher, String> transformers = new HashMap<>();
transformers.put(
return Collections.<ElementMatcher, String>singletonMap(
isMethod()
.and(isPublic())
.and(
named("getResponseCode")
.or(named("connect"))
.or(named("getOutputStream"))
.or(named("getInputStream"))
.or(nameStartsWith("getHeaderField"))),
.and(named("connect").or(named("getOutputStream")).or(named("getInputStream"))),
HttpUrlConnectionAdvice.class.getName());
return transformers;
}

public static class HttpUrlConnectionAdvice {
Expand All @@ -82,28 +69,42 @@ public static Scope startSpan(

final HttpURLState state = HttpURLState.get(thiz);
String operationName = "http.request";
if ("connect".equals(methodName)) {
if (connected) {
return null;
}
// We get here in cases where connect() is called first.
// We need to inject headers now because after connected=true no more headers can be added.

// In total there will be two spans:
// - one for the connect() which does propagation
// - one for the input or output stream (presumably called after connect())
operationName += ".connect";
} else {
if (state.hasDoneIO()) {
return null;
}
state.setHasDoneIO(true);
switch (methodName) {
case "connect":
if (connected) {
return null;
}
/*
* Ideally, we would like to only have a single span for each of the output and input streams,
* but since headers are also sent on connect(), there wouldn't be a span to mark as parent if
* we don't create a span here.
*/
operationName += ".connect";
break;

case "getOutputStream":
if (state.calledOutputStream) {
return null;
}
state.calledOutputStream = true;
operationName += ".output-stream";
break;

case "getInputStream":
if (state.calledInputStream) {
return null;
}
state.calledInputStream = true;
operationName += ".input-stream";
break;
}

// AgentWriter uses HttpURLConnection to report to the trace-agent. We don't want to trace
// those requests.
// Check after the connected test above because getRequestProperty will throw an exception if
// already connected.
/*
* AgentWriter uses HttpURLConnection to report to the trace-agent. We don't want to trace
* those requests. Check after the connected test above because getRequestProperty will
* throw an exception if already connected.
*/
final boolean isTraceRequest =
Thread.currentThread().getName().equals("dd-agent-writer")
|| (!connected && thiz.getRequestProperty("Datadog-Meta-Lang") != null);
Expand All @@ -113,7 +114,7 @@ public static Scope startSpan(

final Tracer tracer = GlobalTracer.get();
if (tracer.activeSpan() == null) {
// httpurlconnection doesn't play nicely with top-level spans
// We don't want this as a top level span.
return null;
}

Expand Down Expand Up @@ -143,8 +144,6 @@ public static Scope startSpan(
}
Tags.HTTP_METHOD.set(span, thiz.getRequestMethod());

tracer.inject(
span.context(), Format.Builtin.HTTP_HEADERS, new MessageHeadersInjectAdapter(thiz));
return scope;
}

Expand All @@ -163,9 +162,11 @@ public static void stopSpan(
Tags.ERROR.set(span, true);
span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
} else if (responseCode > 0) {
// responseCode field cache is sometimes not populated.
// We can't call getResponseCode() due to some unwanted side-effects (e.g. breaks
// getOutputStream).
/*
* responseCode field cache is sometimes not populated.
* We can't call getResponseCode() due to some unwanted side-effects
* (e.g. breaks getOutputStream).
*/
Tags.HTTP_STATUS.set(span, responseCode);
}
scope.close();
Expand All @@ -180,23 +181,21 @@ public static class HttpURLState {
public static HttpURLState get(final HttpURLConnection connection) {
HttpURLState state = STATE_MAP.get(connection);
if (state == null) {
// not thread-safe, but neither is HttpURLConnection
state = new HttpURLState();
STATE_MAP.put(connection, state);
synchronized (connection) {
// might not be a good idea to synchronize on a method parameter...
state = STATE_MAP.get(connection);
if (state == null) {
state = new HttpURLState();
STATE_MAP.put(connection, state);
}
}
}
return state;
}

public boolean hasDoneIO = false;
public boolean calledOutputStream = false;
public boolean calledInputStream = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above comment says // not thread-safe, but neither is HttpURLConnection. I think this is only partially true: sun.net.www.protocol.http.HttpURLConnection seems to be thread-safe. That's probably not a huge issue in reality, but comment may be confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it threadsafe. Though I'm not sure if it's a problem synchronizing on the connection.


private HttpURLState() {}

public boolean hasDoneIO() {
return hasDoneIO;
}

public void setHasDoneIO(final boolean value) {
hasDoneIO = value;
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package datadog.trace.instrumentation.http_url_connection;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMap;
import io.opentracing.util.GlobalTracer;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatcher;
import sun.net.www.MessageHeader;
import sun.net.www.http.HttpClient;

@AutoService(Instrumenter.class)
public class SunHttpClientInstrumentation extends Instrumenter.Default {

public SunHttpClientInstrumentation() {
super("httpurlconnection");
}

@Override
protected boolean defaultEnabled() {
return false;
}

@Override
public ElementMatcher typeMatcher() {
return named("sun.net.www.http.HttpClient");
}

@Override
public String[] helperClassNames() {
return new String[] {
SunHttpClientInstrumentation.class.getName() + "$MessageHeadersInjectAdapter"
};
}

@Override
public Map<ElementMatcher, String> transformers() {
return Collections.<ElementMatcher, String>singletonMap(
isMethod()
.and(isPublic())
.and(named("writeRequests"))
.and(takesArgument(0, named("sun.net.www.MessageHeader")))
// exclude the delegating method:
.and(takesArguments(1).or(takesArguments(2))),
InjectAdvice.class.getName());
}

public static class InjectAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void inject(
@Advice.Argument(0) final MessageHeader header, @Advice.This final HttpClient client) {
final Tracer tracer = GlobalTracer.get();
final Span span = tracer.activeSpan();

if (span != null) {
tracer.inject(
span.context(), Format.Builtin.HTTP_HEADERS, new MessageHeadersInjectAdapter(header));
}
}
}

public static class MessageHeadersInjectAdapter implements TextMap {

private final MessageHeader header;

public MessageHeadersInjectAdapter(final MessageHeader header) {
this.header = header;
}

@Override
public void put(final String key, final String value) {
header.setIfNotSet(key, value);
}

@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException(
"This class should be used only with tracer#inject()");
}
}
}