Skip to content

Commit

Permalink
feature: add MethodHook
Browse files Browse the repository at this point in the history
  • Loading branch information
EddeCCC committed Sep 30, 2024
1 parent 76a44c8 commit 7012135
Show file tree
Hide file tree
Showing 32 changed files with 378 additions and 123 deletions.
3 changes: 2 additions & 1 deletion inspectit-gepard-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@ dependencies {
testImplementation("org.mock-server:mockserver-junit-jupiter:5.15.0")
testImplementation 'org.testcontainers:mockserver:1.20.0'
testImplementation 'org.testcontainers:junit-jupiter:1.20.0'
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.24.0'
testImplementation 'org.mock-server:mockserver-netty:5.15.0'
testImplementation("com.squareup.okhttp3:okhttp:4.12.0")
testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.3.2-alpha")
testImplementation("com.google.protobuf:protobuf-java-util:3.25.4")
// Reading Files from Resources easier
implementation 'commons-io:commons-io:2.16.1'
testImplementation 'commons-io:commons-io:2.16.1'

// Otel Java instrumentation that we use and extend during integration tests
otel("io.opentelemetry.javaagent:opentelemetry-javaagent:${versions.opentelemetryJavaagent}")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import net.bytebuddy.agent.builder.AgentBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rocks.inspectit.gepard.agent.bootstrap.InspectitBootstrapManager;
import rocks.inspectit.gepard.agent.bootstrap.BootstrapManager;
import rocks.inspectit.gepard.agent.configuration.ConfigurationManager;
import rocks.inspectit.gepard.agent.instrumentation.InstrumentationManager;
import rocks.inspectit.gepard.agent.instrumentation.hook.MethodHookManager;
import rocks.inspectit.gepard.agent.instrumentation.state.InstrumentationState;
import rocks.inspectit.gepard.agent.instrumentation.state.configuration.ConfigurationResolver;
import rocks.inspectit.gepard.agent.instrumentation.state.configuration.InspectitConfigurationHolder;
import rocks.inspectit.gepard.agent.notification.NotificationManager;
import rocks.inspectit.gepard.agent.state.ConfigurationResolver;
import rocks.inspectit.gepard.agent.state.InspectitConfigurationHolder;
import rocks.inspectit.gepard.agent.state.InstrumentationState;
import rocks.inspectit.gepard.agent.transformation.TransformationManager;

@SuppressWarnings("unused")
Expand All @@ -34,33 +35,34 @@ public AgentBuilder extend(AgentBuilder agentBuilder, ConfigProperties config) {
log.info("Starting inspectIT Gepard agent extension ...");

// Append the bootstrap classloader with inspectIT interfaces
InspectitBootstrapManager bootstrapManager = InspectitBootstrapManager.create();
BootstrapManager bootstrapManager = BootstrapManager.create();
bootstrapManager.appendToBootstrapClassLoader();

// Notify configuration server about this agent
NotificationManager notificationManager = NotificationManager.create();
notificationManager.sendStartNotification();

// Set up methods hooks to execute inspectIT code inside target applications
MethodHookManager methodHookManager = MethodHookManager.create();

// Prepare instrumentation state tracking
InspectitConfigurationHolder configurationHolder = InspectitConfigurationHolder.create();
ConfigurationResolver configurationResolver = ConfigurationResolver.create(configurationHolder);
InstrumentationState instrumentationState = InstrumentationState.create(configurationResolver);

// Set up methods hooks to execute inspectIT code inside target applications
HookManager hookManager = HookManager.create();
InstrumentationState instrumentationState =
InstrumentationState.create(configurationResolver, methodHookManager);

// Modify the OTel AgentBuilder with our transformer
TransformationManager transformationManager =
TransformationManager.create(instrumentationState);
agentBuilder = transformationManager.modify(agentBuilder);

// Set up instrumentation
// Set up dynamic instrumentation
InstrumentationManager instrumentationManager = InstrumentationManager.create();
instrumentationManager.createConfigurationReceiver();
instrumentationManager.startClassDiscovery();
instrumentationManager.startBatchInstrumentation(instrumentationState);

// Start loading the inspectit configuration
// Start loading the inspectIT configuration
ConfigurationManager configurationManager = ConfigurationManager.create();
configurationManager.loadConfiguration();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@
import org.slf4j.LoggerFactory;

/**
* This manager should append our bootstrap classes to the bootstrap classloader, so there are
* This manager should append our bootstrap classes to the bootstrap classloader, so they are
* accessible globally in the target application as well as this agent.
*/
public class InspectitBootstrapManager {
private static final Logger log = LoggerFactory.getLogger(InspectitBootstrapManager.class);
public class BootstrapManager {
private static final Logger log = LoggerFactory.getLogger(BootstrapManager.class);

private static final String INSPECTIT_BOOTSTRAP_JAR_PATH = "/inspectit-gepard-bootstrap.jar";

private static final String INSPECTIT_BOOTSTRAP_JAR_TEMP_PREFIX = "gepard-bootstrap-";

private InspectitBootstrapManager() {}
private BootstrapManager() {}

/**
* Factory method to create an {@link InspectitBootstrapManager}
* Factory method to create an {@link BootstrapManager}
*
* @return the created manager
*/
public static InspectitBootstrapManager create() {
return new InspectitBootstrapManager();
public static BootstrapManager create() {
return new BootstrapManager();
}

/** Appends our inspectit-gepard-bootstrap.jar to the bootstrap-classloader */
Expand All @@ -57,8 +57,7 @@ public synchronized void appendToBootstrapClassLoader() {
*/
@VisibleForTesting
JarFile copyJarFile(String resourcePath, String prefix) throws IOException {
try (InputStream inputStream =
InspectitBootstrapManager.class.getResourceAsStream(resourcePath)) {
try (InputStream inputStream = BootstrapManager.class.getResourceAsStream(resourcePath)) {

File targetFile = prepareTempFile(prefix);
Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import rocks.inspectit.gepard.agent.instrumentation.cache.input.ClassDiscoveryService;
import rocks.inspectit.gepard.agent.instrumentation.cache.input.ConfigurationReceiver;
import rocks.inspectit.gepard.agent.instrumentation.cache.process.BatchInstrumenter;
import rocks.inspectit.gepard.agent.instrumentation.state.InstrumentationState;
import rocks.inspectit.gepard.agent.internal.configuration.observer.ConfigurationReceivedEvent;
import rocks.inspectit.gepard.agent.internal.schedule.InspectitScheduler;
import rocks.inspectit.gepard.agent.state.InstrumentationState;

/** Responsible component for setting up and executing instrumentation. */
public class InstrumentationManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rocks.inspectit.gepard.agent.instrumentation.cache.PendingClassesCache;
import rocks.inspectit.gepard.agent.instrumentation.state.InstrumentationState;
import rocks.inspectit.gepard.agent.internal.schedule.NamedRunnable;
import rocks.inspectit.gepard.agent.state.InstrumentationState;

/**
* Responsible for retransforming classes in batches. The batch size is fixed to 1000. This is
Expand Down Expand Up @@ -67,17 +67,16 @@ Set<Class<?>> getNextBatch(int batchSize) {

try {
boolean shouldRetransform = instrumentationState.shouldRetransform(clazz);

if (shouldRetransform) classesToRetransform.add(clazz);
} catch (Exception e) {
log.error("Could not check instrumentation status for {}", clazz.getName(), e);
log.error("Could not check instrumentation state for {}", clazz.getName(), e);
}

if (checkedClassesCount >= batchSize) break;
}

log.debug(
"Checked configuration of {} classes, {} classes left to check",
"Checked instrumentation state of {} classes, {} classes left to check",
checkedClassesCount,
pendingClassesCache.getSize());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agent.instrumentation.hook;

import java.util.Objects;
import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook;

/**
* Each {@link MethodHook} instance defines for a single method which actions are performed. This
* defines for example which generic actions are executed or which metrics are collected. Currently,
* we just log our method calls.
*/
public class MethodHook implements IMethodHook {

@Override
public void onEnter(Object[] instrumentedMethodArgs, Object thiz) {
// Using our log4j does not work here...
String message =
String.format(
"inspectIT: Enter MethodHook with %d args in %s",
instrumentedMethodArgs.length, thiz.getClass().getName());
System.out.println(message);
System.out.println("HELLO GEPARD");
}

@Override
public void onExit(
Object[] instrumentedMethodArgs, Object thiz, Object returnValue, Throwable thrown) {
// Using our log4j does not work here...
String exceptionMessage = Objects.nonNull(thrown) ? thrown.getMessage() : "no exception";
String message =
String.format(
"inspectIT: Exit MethodHook who returned %s and threw %s",
returnValue.toString(), exceptionMessage);
System.out.println(message);
System.out.println("BYE GEPARD");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* (C) 2024 */
package rocks.inspectit.gepard.agent.instrumentation.hook;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rocks.inspectit.gepard.agent.instrumentation.hook.configuration.ClassHookConfiguration;
import rocks.inspectit.gepard.agent.instrumentation.hook.configuration.HookedMethods;
import rocks.inspectit.gepard.agent.internal.instrumentation.model.ClassInstrumentationConfiguration;
import rocks.inspectit.gepard.bootstrap.Instances;
import rocks.inspectit.gepard.bootstrap.instrumentation.IHookManager;
import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook;
import rocks.inspectit.gepard.bootstrap.instrumentation.noop.NoopHookManager;
import rocks.inspectit.gepard.bootstrap.instrumentation.noop.NoopMethodHook;

/**
* TODO implement, add java doc
*
* <p>Note: In inspectIT Ocelot we don't implement {@link IHookManager} directly, but instead pass
* over {@link #getHook} as lambda to {@link Instances}. This should avoid issues with spring
* annotation scanning. However, since we don't use Spring at the moment, we directly implement the
* interface.
*/
public class MethodHookManager implements IHookManager {
private static final Logger log = LoggerFactory.getLogger(MethodHookManager.class);

/** Stores classes and all of their hooked methods. Will be kept up-to-date during runtime. */
private final Cache<Class<?>, HookedMethods> hooks;

private MethodHookManager() {
hooks = Caffeine.newBuilder().weakKeys().build();
}

/**
* Factory method to create an {@link MethodHookManager}.
*
* @return the created manager
*/
public static MethodHookManager create() {
log.debug("Creating MethodHookManager...");
if (!Instances.hookManager.equals(NoopHookManager.INSTANCE))
throw new IllegalStateException("Global HookManager already set");

MethodHookManager methodHookManager = new MethodHookManager();
Instances.hookManager = methodHookManager;
addShutdownHook();
return methodHookManager;
}

@Override
public IMethodHook getHook(Class<?> clazz, String methodSignature) {
HookedMethods hookedMethods = hooks.getIfPresent(clazz);
if (Objects.nonNull(hookedMethods)) return hookedMethods.getActiveHook(methodSignature);
return NoopMethodHook.INSTANCE;
}

public void updateHooksFor(Class<?> clazz, ClassInstrumentationConfiguration configuration) {
log.info("Updating hooks for {}", clazz.getName());
ElementMatcher.Junction<MethodDescription> methodMatcher = configuration.methodMatcher();
TypeDescription type = TypeDescription.ForLoadedType.of(clazz);
Set<MethodDescription.InDefinedShape> matchedMethods =
type.getDeclaredMethods().stream()
.filter(methodMatcher::matches)
.collect(Collectors.toSet());

ClassHookConfiguration classConfiguration = new ClassHookConfiguration();
if (!matchedMethods.isEmpty()) matchedMethods.forEach(classConfiguration::putHookConfiguration);

removeObsoleteHooks(clazz, matchedMethods);
updateHooks(clazz, classConfiguration);
}

private void removeObsoleteHooks(
Class<?> clazz, Set<MethodDescription.InDefinedShape> matchedMethods) {
Set<String> matchedSignatures =
matchedMethods.stream().map(this::getSignature).collect(Collectors.toSet());

HookedMethods hookedMethods = hooks.get(clazz, c -> new HookedMethods());
Set<String> methodSignatures = hookedMethods.getMethodSignatures();

AtomicInteger operationCounter = new AtomicInteger(0);
methodSignatures.stream()
.filter(signature -> !matchedSignatures.contains(signature))
.forEach(
signature -> {
removeHook(clazz, signature);
operationCounter.addAndGet(1);
});
log.debug("Removed {} obsolete method hooks for {}", operationCounter.get(), clazz.getName());
}

private void updateHooks(Class<?> clazz, ClassHookConfiguration classConfiguration) {
AtomicInteger operationCounter = new AtomicInteger(0);
classConfiguration
.asMap()
.forEach(
(method, active) -> {
// Currently always true, later we should compare the current with the new config
if (active) {
String signature = getSignature(method);
Optional<MethodHook> maybeHook = getCurrentHook(clazz, signature);
if (maybeHook.isEmpty()) {
setHook(clazz, signature, new MethodHook());
operationCounter.addAndGet(1);
}
}
});
log.info("Updated {} method hooks for {}", operationCounter.get(), clazz.getName());
}

// TODO Alternativen suchen + Methode auslagern
// Wichtig, dass die Signaturen mit denen von ByteBuddy passen!
private String getSignature(MethodDescription methodDescription) {
String methodName = methodDescription.getName();
String parameters =
methodDescription.getParameters().asTypeList().stream()
.map(type -> type.asErasure().getTypeName())
.collect(Collectors.joining(","));
return methodName + "(" + parameters + ")";
}

private void setHook(Class<?> declaringClass, String methodSignature, MethodHook newHook) {
hooks
.asMap()
.computeIfAbsent(declaringClass, (v) -> new HookedMethods())
.putMethod(methodSignature, newHook);
}

private void removeHook(Class<?> declaringClass, String methodSignature) {
HookedMethods hookedMethods = hooks.getIfPresent(declaringClass);
if (Objects.nonNull(hookedMethods)) {
hookedMethods.removeMethod(methodSignature);
if (hookedMethods.noActiveHooks()) hooks.invalidate(declaringClass);
}
}

private Optional<MethodHook> getCurrentHook(Class<?> clazz, String methodSignature) {
HookedMethods hookedMethods = hooks.getIfPresent(clazz);
return Optional.ofNullable(hookedMethods)
.map(methods -> methods.getActiveHook(methodSignature));
}

/** Remove at shutdown */
private static void addShutdownHook() {
Runtime.getRuntime()
.addShutdownHook(new Thread(() -> Instances.hookManager = NoopHookManager.INSTANCE));
}
}
Loading

0 comments on commit 7012135

Please sign in to comment.