-
-
Notifications
You must be signed in to change notification settings - Fork 828
Description
I've been working on a new Java agent that instruments a few bootstrap classes and one Netty class. I'm setting up the agent as this, with a few options to allow for retransformation etc.
AgentBuilder builder = new AgentBuilder.Default()
.with(new AgentBuilder.LocationStrategy() {
@Override
public ClassFileLocator classFileLocator(ClassLoader classLoader, JavaModule module) {
return ClassFileLocator.ForClassLoader.of(classLoader);
}
})
.disableClassFormatChanges()
.ignore(none())
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
.with(AgentBuilder.TypeStrategy.Default.REDEFINE)
;
I'm later using ClassInjector injector = ClassInjector.UsingInstrumentation.of(tempDir, BOOTSTRAP, instrumentation);
to inject all classes that I want into the bootstrap class loader. Full code is available here, although I haven't cleaned up things yet.
I'm opening an issue about something I encountered, which I think it might be a bytebuddy bug, but I'm not 100% sure.
For my use-case, I'm particularly interested to have this agent dynamically loaded, and it works as expected except in one specific scenario. Namely, I'm able to instrument with dynamic injection various applications, ranging from simple Java programs performing outbound HTTP calls or SpringBoot application etc. However, through a user bug report, I encountered a SpringBoot application which was built using the Netty reactor packages for the HTTP web client. This application doesn't work as expected.
Namely, this little bit of code present in the application Jar, breaks dynamic injection in a way that ByteBuddy cannot discover the classes set to be instrumented with advices, but only those:
(sample code)
package com.example.httpclient;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.HttpProtocol;
import java.time.Duration;
@Service
public class HttpClientService {
private HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(
io.netty.handler.ssl.SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
))
.protocol(HttpProtocol.HTTP11);
private final WebClient webClient;
public HttpClientService() {
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
.build();
}
/**
* Makes a GET request to the specified URL
*/
public String makeGetRequest(String url) {
try {
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.block();
} catch (Exception e) {
return "Error making request: " + e.getMessage();
}
}
}
I don't know which one of those packages causes the issue, but I suspect is not reactor.core.publisher.Mono
, because I used that in another place and had no issues.
So this is essentially what happens:
- If I use -agentlib on the Java command line then instrumentation works, in all cases.
- If I try to dynamically inject, no instrumentations work if that reactor.netty web client is used. When I enable the
AgentBuilder.Listener.StreamWriting
printer I can see that ByteBuddy doesn't find any of the classes that I want to instrument. It does find many other classes from the same class loader, but just not the ones that I have in my advices. - To further investigate this, I made a simple advice on the bootclass loader, saying that I want to instrument all methods in
java.lang.Integer
. Again when I use this with -agentlib on the command line, it works well. When I load dynamically, the print output doesn't listjava.lang.Integer
. It does findjava.lang.Integer$IntegerCache
suprisingly, and I do see something like:
[Byte Buddy] DISCOVERY java.lang.Integer$IntegerCache [null, module java.base, Thread[#68,Attach Listener,9,system], loaded=true]
[Byte Buddy] IGNORE java.lang.Integer$IntegerCache [null, module java.base, Thread[#68,Attach Listener,9,system], loaded=true]
[Byte Buddy] COMPLETE java.lang.Integer$IntegerCache [null, module java.base, Thread[#68,Attach Listener,9,system], loaded=true]
- Then from the output I saw that ByteBuddy discovered
java.lang.String
, so I changed my advice to instrumentjava.lang.String
instead, and then it foundjava.lang.Integer
but notjava.lang.String
:-).
So being desperate I tried a workaround, after the agentmain
calls premain
, I manually called inst.getAllLoadedClasses()
, looked for java.lang.Integer
and I called inst.retransformClasses(clazz);
on it. Then the instrumentation worked! So I have a workaround for now, but I thought I'd open an issue to see if this is some sort of a side-effect of how I've set things up or, a genuine bug in bytebuddy. It seems to me that it's related to discovery.
Since this is Netty and Java, I tried the OpenTelemetry Java Agent to see if this is perhaps an issue with how I use the AgentBuilder code or something else. They have the same problem it seems. If I instrument this test application that includes that netty reactor client, and I use -agentlib
to load the Agent on boot then the instrumentation works. But, if I dynamically inject the OpenTelemetry Java Agent, their instrumentation doesn't work just like mine doesn't.