-
Notifications
You must be signed in to change notification settings - Fork 1k
Make OkHttp3 instrumentation more reliable #2894
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,20 +6,24 @@ | |
| package io.opentelemetry.javaagent.instrumentation.okhttp.v3_0; | ||
|
|
||
| import static java.util.Collections.singletonList; | ||
| import static java.util.Collections.singletonMap; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isConstructor; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.returns; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import io.opentelemetry.javaagent.tooling.InstrumentationModule; | ||
| import io.opentelemetry.javaagent.tooling.TypeInstrumentation; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
| import okhttp3.Interceptor; | ||
| import okhttp3.OkHttpClient; | ||
|
|
||
| @AutoService(InstrumentationModule.class) | ||
|
|
@@ -42,16 +46,65 @@ public ElementMatcher<TypeDescription> typeMatcher() { | |
|
|
||
| @Override | ||
| public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() { | ||
| return singletonMap( | ||
| Map<ElementMatcher.Junction<MethodDescription>, String> transformers = new HashMap<>(); | ||
| transformers.put( | ||
| isConstructor().and(takesArgument(0, named("okhttp3.OkHttpClient$Builder"))), | ||
| OkHttp3InstrumentationModule.class.getName() + "$OkHttp3Advice"); | ||
| OkHttp3InstrumentationModule.class.getName() + "$OkHttp3ClientConstructorAdvice"); | ||
| transformers.put( | ||
| isMethod().and(named("newBuilder").and(returns(named("okhttp3.OkHttpClient$Builder")))), | ||
| OkHttp3InstrumentationModule.class.getName() + "$OkHttp3ClientNewBuilderAdvice"); | ||
| return transformers; | ||
| } | ||
| } | ||
|
|
||
| public static class OkHttp3Advice { | ||
| // This advice makes two guarantees: | ||
| // 1) The state of the builder (specifically interceptor list) is the same before and after the | ||
| // OkHttpClient constructor invocation | ||
| // 2) The interceptor list of the created OkHttpClient has exactly one instance of the tracing | ||
| // interceptor and it is in the end (assuming no other instrumentations) | ||
| public static class OkHttp3ClientConstructorAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void addTracingInterceptor(@Advice.Argument(0) OkHttpClient.Builder builder) { | ||
| public static void addTracingInterceptor( | ||
| @Advice.Argument(0) OkHttpClient.Builder builder, | ||
| @Advice.Local("otelOriginalInterceptors") List<Interceptor> originalInterceptors) { | ||
|
|
||
| if (builder.interceptors().contains(OkHttp3Interceptors.TRACING_INTERCEPTOR)) { | ||
| // Potential corner case - the tracing interceptor may be in the builder due to the builder | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the difference vs just calling return without the state management?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A possible case is that the tracing interceptor may be in the middle, therefore it captures a "request" that may not be the actual request at all due to some later interceptor deciding to do something different.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it can be in the middle if we always add it in the constructor (unless already present which is only when constructing a builder from another client, where it again couldn't be in the middle).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible - the third test in |
||
| // being manually constructed by adding interceptors from an existing client. In this case, | ||
| // save the original interceptors so we can restore them after the constructor call, and | ||
| // then remove all tracing interceptors before adding one as the last. | ||
| originalInterceptors = new ArrayList<>(builder.interceptors()); | ||
|
|
||
| while (builder.interceptors().remove(OkHttp3Interceptors.TRACING_INTERCEPTOR)) {} | ||
| } | ||
|
|
||
| builder.addInterceptor(OkHttp3Interceptors.TRACING_INTERCEPTOR); | ||
| } | ||
|
|
||
| @Advice.OnMethodExit(suppress = Throwable.class) | ||
| public static void removeTracingInterceptor( | ||
| @Advice.Argument(0) OkHttpClient.Builder builder, | ||
| @Advice.Local("otelOriginalInterceptors") List<Interceptor> originalInterceptors) { | ||
|
|
||
| // Restore the interceptor list to what it was before the constructor call. In the common | ||
| // case, a single instance of the tracing interceptor was appended to the end, so it will be | ||
| // removed. For the corner case where builder already contained the tracing interceptor, the | ||
| // original interceptor list was saved to a local and will be restored from there. | ||
| if (originalInterceptors != null) { | ||
| builder.interceptors().clear(); | ||
| builder.interceptors().addAll(originalInterceptors); | ||
| } else { | ||
| builder.interceptors().remove(OkHttp3Interceptors.TRACING_INTERCEPTOR); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static class OkHttp3ClientNewBuilderAdvice { | ||
| @Advice.OnMethodExit(suppress = Throwable.class) | ||
| public static void removeTracingInterceptor(@Advice.Return OkHttpClient.Builder builder) { | ||
| // Remove the interceptor from the builder returned by newBuilder, as it should be added only | ||
| // by the constructor instrumentation to guarantee that it is the last one in the chain. | ||
| builder.interceptors().remove(OkHttp3Interceptors.TRACING_INTERCEPTOR); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this relying on an implementation detail that the interceptor is applied directly to a client? Otherwise I don't see how it can work otherwise if we remove it right away. But I think we shouldn't rely on such a special implementation detail, it seems correct for the builder to have our interceptor when it's constructed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the builder not affecting any objects that have already been built in the past by this builder definitely does not sound like an implementation detail to me, but a very logical part of how the builder pattern works. Unless I misunderstood what you meant by the interceptor being applied to the client directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think it is highly unexpected that a call to
buildmutates the builder itself. This is not usually done by builders and therefore adding such a side effect to that method would not be good instrumentation etiquette.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah if this was the build method I'd get it but it's the builder constructor isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor is effectively filling the role of what is usually done by a
buildmethod (in cases where an instance can also be created without a builder), and is private and only called from thebuildmethod and the zero-parameter constructor. However, being a constructor does not change that it should leave the builder in the same state, and that the builder can be modified and reused later to build a different client instance.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh shoot I had been misreading the constructor to be of the builder, not the client. Sorry for the confusion makes sense now. Is it possible to simplify by changing to instrument the builder constructor to prepopulate interceptors if it doesn't already have it and not instrument the client itself at all?