diff --git a/README.md b/README.md index fb9229a..2ee7455 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ inspectIT Gepard is the further development of [inspectIT Ocelot](https://github While the inspectIT Ocelot Java agent is self-made, inspectIT Gepard uses the OpenTelemetry Java agent as basis and extends it with features from inspectIT Ocelot. +## Main Features + +We want to enable **dynamic instrumentation** with the OpenTelemetry agent. +This means, that you can change your instrumentation **during runtime**. +For example, you can add new methods to your tracing or record data for new metrics, everything without restarting your application! + +The instrumentation configuration will be fetched from a remote server regularly. +The server's url is configurable via the property `inspectit.config.http.url` + +We are also developing our own [configuration server](https://github.com/inspectIT/inspectit-gepard-agentmanager). + ## Installation To build this extension project, run `./gradlew build` or `./gradlew extendedAgent` (no tests). diff --git a/inspectit-gepard-agent/build.gradle b/inspectit-gepard-agent/build.gradle index b79f106..f54bc91 100644 --- a/inspectit-gepard-agent/build.gradle +++ b/inspectit-gepard-agent/build.gradle @@ -120,8 +120,8 @@ dependencies { 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 - testImplementation 'commons-io:commons-io:2.16.1' + // Reading files from resources easier + 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}") diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/InspectitAgentExtension.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/InspectitAgentExtension.java index b932031..12b7b03 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/InspectitAgentExtension.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/InspectitAgentExtension.java @@ -2,6 +2,7 @@ package rocks.inspectit.gepard.agent; import com.google.auto.service.AutoService; +import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.javaagent.tooling.AgentExtension; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import net.bytebuddy.agent.builder.AgentBuilder; @@ -15,6 +16,7 @@ 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.internal.otel.OpenTelemetryAccessor; import rocks.inspectit.gepard.agent.notification.NotificationManager; import rocks.inspectit.gepard.agent.transformation.TransformationManager; @@ -43,6 +45,9 @@ public AgentBuilder extend(AgentBuilder agentBuilder, ConfigProperties config) { NotificationManager notificationManager = NotificationManager.create(); notificationManager.sendStartNotification(); + // Set our global OpenTelemetry instance. For now, we use the Agent SDK + OpenTelemetryAccessor.setOpenTelemetry(GlobalOpenTelemetry.get()); + // Set up methods hooks to execute inspectIT code inside target applications MethodHookManager methodHookManager = MethodHookManager.create(new MethodHookState()); diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHook.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHook.java index 19710f5..d9452bf 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHook.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHook.java @@ -2,6 +2,10 @@ package rocks.inspectit.gepard.agent.instrumentation.hook; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.SpanAction; +import rocks.inspectit.gepard.bootstrap.context.InternalInspectitContext; import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook; /** @@ -10,28 +14,62 @@ * we just log our method calls. */ public class MethodHook implements IMethodHook { + private static final Logger log = LoggerFactory.getLogger(MethodHook.class); + + private final String methodName; + + private final SpanAction spanAction; + + public MethodHook(String methodName, SpanAction spanAction) { + this.methodName = methodName; + this.spanAction = spanAction; + } @Override - public void onEnter(Object[] instrumentedMethodArgs, Object thiz) { - // Using our log4j here will not be visible in the target application... + public InternalInspectitContext onEnter(Object[] instrumentedMethodArgs, Object thiz) { 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"); + + String spanName = thiz.getClass().getSimpleName() + "." + methodName; + AutoCloseable spanScope = null; + + try { + spanScope = spanAction.startSpan(spanName); + } catch (Exception e) { + log.error("Could not execute start-span-action", e); + } + + // Using our log4j here will not be visible in the target application... + System.out.println("HELLO GEPARD : " + methodName); + return new InternalInspectitContext(this, spanScope); } @Override public void onExit( - Object[] instrumentedMethodArgs, Object thiz, Object returnValue, Throwable thrown) { - // Using our log4j here will not be visible in the target application... + InternalInspectitContext context, + Object[] instrumentedMethodArgs, + Object thiz, + Object returnValue, + Throwable thrown) { String exceptionMessage = Objects.nonNull(thrown) ? thrown.getMessage() : "no exception"; + String returnMessage = Objects.nonNull(returnValue) ? returnValue.toString() : "nothing"; String message = String.format( "inspectIT: Exit MethodHook who returned %s and threw %s", - returnValue.toString(), exceptionMessage); + returnMessage, exceptionMessage); System.out.println(message); + + AutoCloseable spanScope = context.getSpanScope(); + try { + spanAction.endSpan(spanScope); + } catch (Exception e) { + log.error("Could not execute end-span-action", e); + } + + // Using our log4j here will not be visible in the target application... System.out.println("BYE GEPARD"); } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookState.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookState.java index c619c5f..5ffdeaf 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookState.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookState.java @@ -12,6 +12,7 @@ import net.bytebuddy.description.method.MethodDescription; import rocks.inspectit.gepard.agent.instrumentation.hook.configuration.ClassHookConfiguration; import rocks.inspectit.gepard.agent.instrumentation.hook.configuration.HookedMethods; +import rocks.inspectit.gepard.agent.instrumentation.hook.util.MethodHookGenerator; /** Stores the method hook configurations of all instrumented classes. */ public class MethodHookState { @@ -78,7 +79,8 @@ public int updateHooks(Class clazz, ClassHookConfiguration classConfiguration String signature = getSignature(method); Optional maybeHook = getCurrentHook(clazz, signature); if (maybeHook.isEmpty()) { - setHook(clazz, signature, new MethodHook()); + MethodHook hook = MethodHookGenerator.createHook(method); + setHook(clazz, signature, hook); operationCounter.addAndGet(1); } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java new file mode 100644 index 0000000..44f28a6 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanAction.java @@ -0,0 +1,55 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.action; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import java.util.Objects; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.exception.CouldNotCloseSpanScopeException; +import rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor; + +/** This action contains the logic to start and end a {@link Span}. */ +public class SpanAction { + + private static final String INSTRUMENTATION_SCOPE_NAME = "inspectit-gepard"; + + private final OpenTelemetry openTelemetry; + + public SpanAction() { + this.openTelemetry = OpenTelemetryAccessor.getOpenTelemetry(); + } + + /** + * Starts a new {@link Span}. Should be called before {@link SpanAction#endSpan}. + * + * @param spanName the name of the span + * @return the scope of the started span + */ + public AutoCloseable startSpan(String spanName) { + Span.current().getSpanContext(); + + Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_SCOPE_NAME); + Span span = tracer.spanBuilder(spanName).setParent(Context.current()).startSpan(); + return span.makeCurrent(); + } + + /** + * Ends the current span and closes its scope. Should be called after {@link + * SpanAction#startSpan}. + * + * @param spanScope the scope of the span, which should be finished + */ + public void endSpan(AutoCloseable spanScope) { + Span current = Span.current(); + + if (Objects.nonNull(spanScope)) { + try { + spanScope.close(); + } catch (Exception e) { + throw new CouldNotCloseSpanScopeException(e); + } + } + current.end(); + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/exception/CouldNotCloseSpanScopeException.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/exception/CouldNotCloseSpanScopeException.java new file mode 100644 index 0000000..0346e01 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/exception/CouldNotCloseSpanScopeException.java @@ -0,0 +1,12 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.action.exception; + +import io.opentelemetry.context.Scope; + +/** Exception errors, while trying to close a {@link Scope} */ +public class CouldNotCloseSpanScopeException extends RuntimeException { + + public CouldNotCloseSpanScopeException(Throwable cause) { + super("Could not close span scope", cause); + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGenerator.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGenerator.java new file mode 100644 index 0000000..b263938 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGenerator.java @@ -0,0 +1,22 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.util; + +import net.bytebuddy.description.method.MethodDescription; +import rocks.inspectit.gepard.agent.instrumentation.hook.MethodHook; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.SpanAction; + +public class MethodHookGenerator { + + private MethodHookGenerator() {} + + /** + * Creates an executable method hook based on the given configuration. + * + * @param method the hooked method + * @return the created method hook + */ + public static MethodHook createHook(MethodDescription method) { + SpanAction spanAction = new SpanAction(); + return new MethodHook(method.getName(), spanAction); + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java index abfbf98..4323b88 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/instrumentation/state/InstrumentationState.java @@ -122,6 +122,10 @@ private void updateHooks( boolean currentConfigRequiresHooks = Objects.nonNull(currentConfig) && currentConfig.isActive(); if (newConfigRequiresHooks || currentConfigRequiresHooks) - methodHookManager.updateHooksFor(clazz, newConfig); + try { + methodHookManager.updateHooksFor(clazz, newConfig); + } catch (Exception e) { + log.error("There was an error while updating the hooks of class {}", clazz.getName(), e); + } } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java new file mode 100644 index 0000000..f95865f --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/internal/otel/OpenTelemetryAccessor.java @@ -0,0 +1,34 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.internal.otel; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; + +/** + * Singleton to access the {@link OpenTelemetry} instance. We use this accessor, because according + * to the documentation of {@link GlobalOpenTelemetry}, the get() method should only be called once + * during the application. + */ +public class OpenTelemetryAccessor { + + private static OpenTelemetry openTelemetry; + + private OpenTelemetryAccessor() {} + + /** + * @return the global {@link OpenTelemetry} instance + */ + public static OpenTelemetry getOpenTelemetry() { + return openTelemetry; + } + + /** + * Sets the global {@link OpenTelemetry} instance for inspectIT. This will allow us to create + * traces or metrics. Should only be called once. + * + * @param otel the openTelemetry object + */ + public static void setOpenTelemetry(OpenTelemetry otel) { + openTelemetry = otel; + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/InspectitListener.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/InspectitListener.java new file mode 100644 index 0000000..ae40ed5 --- /dev/null +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/InspectitListener.java @@ -0,0 +1,55 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.transformation; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Callbacks, which are executed before or after {@link DynamicTransformer#transform}. Currently, + * just used for debugging transformation. + */ +public class InspectitListener implements AgentBuilder.Listener { + private static final Logger log = LoggerFactory.getLogger(InspectitListener.class); + + @Override + public void onError( + String typeName, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + Throwable throwable) { + log.debug("Dynamic transformation failed for type '{}': {}", typeName, throwable.getMessage()); + } + + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + // unused + } + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + DynamicType dynamicType) { + // unused + } + + @Override + public void onIgnored( + TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) { + // unused + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + // unused + } +} diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java index 85649ec..c8609dc 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/TransformationManager.java @@ -32,7 +32,9 @@ public static TransformationManager create(InstrumentationState instrumentationS */ public AgentBuilder modify(AgentBuilder agentBuilder) { DynamicTransformer transformer = new DynamicTransformer(instrumentationState); + InspectitListener listener = new InspectitListener(); + // In the future, we might add a white- or black-list for types - return agentBuilder.type(any()).transform(transformer); + return agentBuilder.type(any()).transform(transformer).with(listener); } } diff --git a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/advice/InspectitAdvice.java b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/advice/InspectitAdvice.java index 5b3db4f..835c04a 100644 --- a/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/advice/InspectitAdvice.java +++ b/inspectit-gepard-agent/src/main/java/rocks/inspectit/gepard/agent/transformation/advice/InspectitAdvice.java @@ -4,14 +4,25 @@ import net.bytebuddy.asm.Advice; import net.bytebuddy.implementation.bytecode.assign.Assigner; import rocks.inspectit.gepard.bootstrap.Instances; +import rocks.inspectit.gepard.bootstrap.context.InternalInspectitContext; import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook; -/** Static code, which should be injected into target scopes (class methods) */ +/** + * Static code, which will be injected into target scopes (class methods). There are some rules for + * advice classes: + * + * + */ @SuppressWarnings("unused") public class InspectitAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - public static IMethodHook onEnter( + public static InternalInspectitContext onEnter( @Advice.AllArguments Object[] args, @Advice.This Object thiz, @Advice.Origin("#t") Class declaringClass, @@ -21,9 +32,9 @@ public static IMethodHook onEnter( + signature + " of class: " + thiz.getClass().getName()); + IMethodHook hook = Instances.hookManager.getHook(declaringClass, signature); - hook.onEnter(args, thiz); - return hook; + return hook.onEnter(args, thiz); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) @@ -32,13 +43,15 @@ public static void onExit( @Advice.This Object thiz, @Advice.Thrown Throwable throwable, @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returnValue, - @Advice.Origin("#m#s") String signature, - @Advice.Enter IMethodHook hook) { + @Advice.Enter InternalInspectitContext context, + @Advice.Origin("#m#s") String signature) { System.out.println( - "Executing Exit Advice in method: " + "Executing EXIT Advice in method: " + signature + " of class: " + thiz.getClass().getName()); - hook.onExit(args, thiz, returnValue, throwable); + + IMethodHook hook = context.getHook(); + hook.onExit(context, args, thiz, returnValue, throwable); } } diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookStateTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookStateTest.java index b7609a3..11f0756 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookStateTest.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookStateTest.java @@ -27,8 +27,6 @@ class MethodHookStateTest { @Mock private ClassHookConfiguration classHookConfiguration; - @Mock private HookedMethods hookedMethods; - @Mock private MethodHook methodHook; private static final Class TEST_CLASS = MethodHookManagerTest.class; diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookTest.java new file mode 100644 index 0000000..ff8cf7b --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/MethodHookTest.java @@ -0,0 +1,63 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.SpanAction; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.exception.CouldNotCloseSpanScopeException; +import rocks.inspectit.gepard.bootstrap.context.InternalInspectitContext; + +@ExtendWith(MockitoExtension.class) +class MethodHookTest { + + @Mock private SpanAction spanAction; + + @Mock private AutoCloseable closeable; + + @Mock private InternalInspectitContext internalContext; + + private MethodHook hook; + + @BeforeEach + void beforeEach() { + hook = new MethodHook("method", spanAction); + } + + @Test + void shouldStartSpanAndCreateContext() { + when(spanAction.startSpan(anyString())).thenReturn(closeable); + + InternalInspectitContext context = hook.onEnter(new Object[] {}, this); + + verify(spanAction).startSpan(anyString()); + assertEquals(closeable, context.getSpanScope()); + assertEquals(hook, context.getHook()); + } + + @Test + void shouldNotReturnSpanScopeWhenExceptionThrown() { + doThrow(CouldNotCloseSpanScopeException.class).when(spanAction).startSpan(anyString()); + + InternalInspectitContext context = hook.onEnter(new Object[] {}, this); + + verify(spanAction).startSpan(anyString()); + assertNull(context.getSpanScope()); + } + + @Test + void shouldEndSpan() { + when(internalContext.getSpanScope()).thenReturn(closeable); + + hook.onExit(internalContext, new Object[] {}, this, null, null); + + verify(spanAction).endSpan(closeable); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanActionTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanActionTest.java new file mode 100644 index 0000000..8b97a11 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/action/SpanActionTest.java @@ -0,0 +1,51 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.gepard.agent.instrumentation.hook.action.exception.CouldNotCloseSpanScopeException; +import rocks.inspectit.gepard.agent.internal.otel.OpenTelemetryAccessor; + +@ExtendWith(MockitoExtension.class) +class SpanActionTest { + + @Mock private AutoCloseable closeable; + + private final SpanAction action = new SpanAction(); + + @BeforeAll + static void beforeAll() { + OpenTelemetryAccessor.setOpenTelemetry(GlobalOpenTelemetry.get()); + } + + @Test + void shouldCreateScope() { + String spanName = "Test.method"; + + AutoCloseable scope = action.startSpan(spanName); + + assertNotNull(scope); + } + + @Test + void shouldCloseScope() throws Exception { + action.endSpan(closeable); + + verify(closeable).close(); + } + + @Test + void shouldThrowExceptionWhenScopeNotClosable() throws Exception { + doThrow(Exception.class).when(closeable).close(); + + assertThrows(CouldNotCloseSpanScopeException.class, () -> action.endSpan(closeable)); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGeneratorTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGeneratorTest.java new file mode 100644 index 0000000..83b7c78 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/instrumentation/hook/util/MethodHookGeneratorTest.java @@ -0,0 +1,27 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.instrumentation.hook.util; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import net.bytebuddy.description.method.MethodDescription; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.gepard.agent.instrumentation.hook.MethodHook; + +@ExtendWith(MockitoExtension.class) +class MethodHookGeneratorTest { + + @Mock private MethodDescription methodDescription; + + @Test + void shouldCreateMethodHook() { + when(methodDescription.getName()).thenReturn("method"); + + MethodHook hook = MethodHookGenerator.createHook(methodDescription); + + assertNotNull(hook); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java index 82934ed..667768a 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/IntegrationTestBase.java @@ -1,6 +1,8 @@ /* (C) 2024 */ package rocks.inspectit.gepard.agent.integrationtest; +import static rocks.inspectit.gepard.agent.integrationtest.utils.LogUtils.countTimes; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.InvalidProtocolBufferException; @@ -18,6 +20,8 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.ResponseBody; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -39,12 +43,14 @@ public abstract class IntegrationTestBase { private static final Logger logger = LoggerFactory.getLogger(IntegrationTestBase.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); protected static OkHttpClient client = OkHttpUtils.client(); private static final Network network = Network.newNetwork(); + + protected static final String configDir = "integrationtest/configurations/"; + protected static final String agentPath = System.getProperty("io.opentelemetry.smoketest.agentPath"); // Javaagent with extensions embedded inside it @@ -220,4 +226,55 @@ private String waitForContent() throws IOException, InterruptedException { return content; } + + /** + * Waits until the instrumentation was applied in the method hooks for the specified amount of + * times. The test should not fail here, if no further update message was found. + */ + protected void awaitInstrumentationUpdate(int amount) { + String updateMessage = + "method hooks for io.opentelemetry.smoketest.springboot.controller.WebController"; + + try { + awaitUpdateMessage(updateMessage, amount); + } catch (ConditionTimeoutException e) { + System.out.println("No instrumentation update occurred"); + } + } + + /** + * Waits until the configuration was polled one more time. The test should not fail here, if no + * further update message was found. + */ + protected void awaitConfigurationUpdate() { + String updateMessage = + "Fetched configuration from configuration server and received status code 200"; + try { + awaitUpdateMessage(updateMessage, 1); + } catch (ConditionTimeoutException e) { + System.out.println("No configuration update occurred"); + } + } + + /** + * Waits until a certain update message was logged again. This happens via checking the container + * logs. First the method counts the current amount of update messages. If the amount of update + * messages has increased, it is assumed that a new configuration has been pooled. + * + * @param updateMessage the message, which will be waited for + */ + private void awaitUpdateMessage(String updateMessage, int amount) { + String logs = target.getLogs(); + int updateCount = countTimes(logs, updateMessage); + + Awaitility.await() + .pollDelay(5, TimeUnit.SECONDS) + .atMost(15, TimeUnit.SECONDS) + .until( + () -> { + String newLogs = target.getLogs(); + int currentUpdateCount = countTimes(newLogs, updateMessage); + return currentUpdateCount >= updateCount + amount; + }); + } } diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/ScopeTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/ScopeTest.java deleted file mode 100644 index edc7969..0000000 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/ScopeTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* (C) 2024 */ -package rocks.inspectit.gepard.agent.integrationtest.spring; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.concurrent.TimeUnit; -import okhttp3.Call; -import okhttp3.Request; -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionTimeoutException; -import org.junit.jupiter.api.Test; - -public class ScopeTest extends SpringTestBase { - - private static final String configDir = "integrationtest/configurations/"; - - @Test - void scopeWithoutMethodInstrumentsAllMethods() throws Exception { - configurationServerMock.configServerSetup(configDir + "simple-scope.json"); - startTarget("/opentelemetry-extensions.jar"); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - String logs = target.getLogs(); - stopTarget(); - - assertLogs(logs, 2); - } - - @Test - void scopeWithOneMethodInstrumentsOneMethod() throws Exception { - configurationServerMock.configServerSetup(configDir + "scope-with-method.json"); - startTarget("/opentelemetry-extensions.jar"); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - String logs = target.getLogs(); - stopTarget(); - - assertLogs(logs, 1); - } - - @Test - void scopeWithTwoMethodsInstrumentsTwoMethods() throws Exception { - configurationServerMock.configServerSetup(configDir + "scope-with-multiple-methods.json"); - startTarget("/opentelemetry-extensions.jar"); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - String logs = target.getLogs(); - stopTarget(); - - assertLogs(logs, 2); - } - - @Test - void emptyConfigurationDoesntInstrument() throws Exception { - configurationServerMock.configServerSetup(configDir + "empty-configuration.json"); - startTarget("/opentelemetry-extensions.jar"); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - String logs = target.getLogs(); - stopTarget(); - - assertLogs(logs, 0); - } - - @Test - void multipleScopesInstrumentAllSelectedMethods() throws Exception { - configurationServerMock.configServerSetup(configDir + "multiple-scopes.json"); - startTarget("/opentelemetry-extensions.jar"); - // We need to instrument 2 classes - awaitInstrumentationUpdate(2); - - sendRequestToTarget("/greeting"); - sendRequestToTarget("/front"); - - String logs = target.getLogs(); - stopTarget(); - - assertLogs(logs, 4); - } - - @Test - void configurationUpdatesAreApplied() throws Exception { - // Set up config server to instrument multiple methods - configurationServerMock.configServerSetup(configDir + "scope-with-multiple-methods.json"); - startTarget("/opentelemetry-extensions.jar"); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - String logs = target.getLogs(); - - assertLogs(logs, 2); - - // Update configuration to only instrument one method - configurationServerMock.reset(); - configurationServerMock.configServerSetup(configDir + "scope-with-method.json"); - awaitConfigurationUpdate(); - awaitInstrumentationUpdate(1); - - sendRequestToTarget("/greeting"); - logs = target.getLogs(); - stopTarget(); - - // 2 logs before update + 1 log after update - assertLogs(logs, 3); - } - - private void sendRequestToTarget(String path) throws Exception { - String url = String.format("http://localhost:%d%s", target.getMappedPort(8080), path); - Call call = client.newCall(new Request.Builder().url(url).get().build()); - call.execute(); - - // wait for logs - Thread.sleep(1000); - } - - /** - * Checks, if the logs contain "HELLO GEPARD" and "BYE GEPARD" for a specific number of times - * - * @param logs the logs - * @param times the amount of times "HELLO GEPARD" and "BYE GEPARD" should be present in the logs - */ - private void assertLogs(String logs, int times) { - boolean loggedHelloGepardTwice = containsTimes(logs, "HELLO GEPARD", times); - boolean loggedByeGepardTwice = containsTimes(logs, "BYE GEPARD", times); - - assertTrue(loggedHelloGepardTwice); - assertTrue(loggedByeGepardTwice); - } - - /** - * Checks, if a specific message can be found for a specific amount of times inside the provided - * logs. - * - * @return true, if the message appears the expected amount of times in the logs - */ - private boolean containsTimes(String logs, String message, int times) { - int count = countTimes(logs, message); - return count == times; - } - - /** - * Counts how many times a specific message can be found inside the provided logs - * - * @param logs the logs - * @param message the message to look for - * @return the amount of times the message appears in the logs - */ - private int countTimes(String logs, String message) { - int count = 0; - int index = 0; - while (index != -1) { - index = logs.indexOf(message, index); - if (index != -1) { - count++; - index += message.length(); - } - } - return count; - } - - /** - * Waits until the instrumentation was applied in the method hooks for the specified amount of - * times. The test should not fail here, if no further update message was found. - */ - private void awaitInstrumentationUpdate(int amount) { - String updateMessage = - "method hooks for io.opentelemetry.smoketest.springboot.controller.WebController"; - - try { - awaitUpdateMessage(updateMessage, amount); - } catch (ConditionTimeoutException e) { - System.out.println("No instrumentation update occurred"); - } - } - - /** - * Waits until the configuration was polled one more time. The test should not fail here, if no - * further update message was found. - */ - private void awaitConfigurationUpdate() { - String updateMessage = - "Fetched configuration from configuration server and received status code 200"; - try { - awaitUpdateMessage(updateMessage, 1); - } catch (ConditionTimeoutException e) { - System.out.println("No configuration update occurred"); - } - } - - /** - * Waits until a certain update message was logged again. This happens via checking the container - * logs. First the method counts the current amount of update messages. If the amount of update - * messages has increased, it is assumed that a new configuration has been pooled. - * - * @param updateMessage the message, which will be waited for - */ - private void awaitUpdateMessage(String updateMessage, int amount) { - String logs = target.getLogs(); - int updateCount = countTimes(logs, updateMessage); - - Awaitility.await() - .pollDelay(5, TimeUnit.SECONDS) - .atMost(15, TimeUnit.SECONDS) - .until( - () -> { - String newLogs = target.getLogs(); - int currentUpdateCount = countTimes(newLogs, updateMessage); - return currentUpdateCount >= updateCount + amount; - }); - } -} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/SpringTestBase.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/SpringTestBase.java index 333a677..eb725e4 100644 --- a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/SpringTestBase.java +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/SpringTestBase.java @@ -2,6 +2,8 @@ package rocks.inspectit.gepard.agent.integrationtest.spring; import java.time.Duration; +import okhttp3.Call; +import okhttp3.Request; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import rocks.inspectit.gepard.agent.integrationtest.IntegrationTestBase; @@ -21,4 +23,13 @@ protected WaitStrategy getTargetWaitStrategy() { return Wait.forLogMessage(".*Started SpringbootApplication in.*", 1) .withStartupTimeout(Duration.ofMinutes(1)); } + + protected void sendRequestToTarget(String path) throws Exception { + String url = String.format("http://localhost:%d%s", target.getMappedPort(8080), path); + Call call = client.newCall(new Request.Builder().url(url).get().build()); + call.execute(); + + // wait for logs + Thread.sleep(1000); + } } diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java new file mode 100644 index 0000000..9d91e81 --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/scope/ScopeTest.java @@ -0,0 +1,104 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.integrationtest.spring.scope; + +import static rocks.inspectit.gepard.agent.integrationtest.utils.LogUtils.assertLogs; + +import org.junit.jupiter.api.Test; +import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; + +public class ScopeTest extends SpringTestBase { + + @Test + void scopeWithoutMethodInstrumentsAllMethods() throws Exception { + configurationServerMock.configServerSetup(configDir + "simple-scope.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + String logs = target.getLogs(); + stopTarget(); + + assertLogs(logs, 2); + } + + @Test + void scopeWithOneMethodInstrumentsOneMethod() throws Exception { + configurationServerMock.configServerSetup(configDir + "scope-with-method.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + String logs = target.getLogs(); + stopTarget(); + + assertLogs(logs, 1); + } + + @Test + void scopeWithTwoMethodsInstrumentsTwoMethods() throws Exception { + configurationServerMock.configServerSetup(configDir + "scope-with-multiple-methods.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + String logs = target.getLogs(); + stopTarget(); + + assertLogs(logs, 2); + } + + @Test + void emptyConfigurationDoesntInstrument() throws Exception { + configurationServerMock.configServerSetup(configDir + "empty-configuration.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + String logs = target.getLogs(); + stopTarget(); + + assertLogs(logs, 0); + } + + @Test + void multipleScopesInstrumentAllSelectedMethods() throws Exception { + configurationServerMock.configServerSetup(configDir + "multiple-scopes.json"); + startTarget("/opentelemetry-extensions.jar"); + // We need to instrument 2 classes + awaitInstrumentationUpdate(2); + + sendRequestToTarget("/greeting"); + sendRequestToTarget("/front"); + + String logs = target.getLogs(); + stopTarget(); + + assertLogs(logs, 4); + } + + @Test + void configurationUpdatesAreApplied() throws Exception { + // Set up config server to instrument multiple methods + configurationServerMock.configServerSetup(configDir + "scope-with-multiple-methods.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + String logs = target.getLogs(); + + assertLogs(logs, 2); + + // Update configuration to only instrument one method + configurationServerMock.reset(); + configurationServerMock.configServerSetup(configDir + "scope-with-method.json"); + awaitConfigurationUpdate(); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + logs = target.getLogs(); + stopTarget(); + + // 2 logs before update + 1 log after update + assertLogs(logs, 3); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java new file mode 100644 index 0000000..334fb1f --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/spring/trace/TracingTest.java @@ -0,0 +1,130 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.integrationtest.spring.trace; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ScopeSpans; +import io.opentelemetry.proto.trace.v1.Span; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import rocks.inspectit.gepard.agent.integrationtest.spring.SpringTestBase; + +/** Should check, if traces are received in our tracing backend according to our configuration. */ +class TracingTest extends SpringTestBase { + + private static final String parentSpanName = "WebController.greeting"; + + private static final String childSpanName = "WebController.withSpan"; + + @Override + protected Map getExtraEnv() { + // We need to disable the OTel annotations to prevent span duplicates + return Map.of("OTEL_INSTRUMENTATION_OPENTELEMETRY_EXTENSION_ANNOTATIONS_ENABLED", "false"); + } + + @Test + void shouldSendSpansToBackendWhenScopesAreActive() throws Exception { + configurationServerMock.configServerSetup(configDir + "simple-scope.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + Collection traces = waitForTraces(); + + assertSpans(traces, parentSpanName, childSpanName); + } + + @Test + void shouldNotSendSpansToBackendWhenNoScopesAreActive() throws Exception { + configurationServerMock.configServerSetup(configDir + "empty-configuration.json"); + startTarget("/opentelemetry-extensions.jar"); + awaitInstrumentationUpdate(1); + + sendRequestToTarget("/greeting"); + Collection traces = waitForTraces(); + + assertNoSpans(traces, parentSpanName, childSpanName); + } + + /** + * This method asserts that spans with the given names exist and that the child's {@link + * Span#getParentSpanId()} equals the parent's {@link Span#getSpanId()}. + * + * @param traces the collection of traces + * @param parentSpanName the name of the parent span + * @param childSpanName the name of the child span + */ + private void assertSpans( + Collection traces, String parentSpanName, String childSpanName) { + Stream> spanLists = getSpanLists(traces); + + assertTrue( + spanLists.anyMatch( + spans -> { + Optional parentSpan = findSpan(spans, parentSpanName); + if (parentSpan.isEmpty()) return false; + + Optional childSpan = findSpan(spans, childSpanName); + if (childSpan.isEmpty()) return false; + + // We cannot compare the ByteStrings directly, because they are different objects + String childParentId = childSpan.get().getParentSpanId().toStringUtf8(); + String parentId = parentSpan.get().getSpanId().toStringUtf8(); + return childParentId.equals(parentId); + })); + } + + /** + * This method asserts that no spans with the given names exist. + * + * @param traces the collection of traces + * @param parentSpanName the name of the parent span + * @param childSpanName the name of the child span + */ + private void assertNoSpans( + Collection traces, String parentSpanName, String childSpanName) { + Stream> spanLists = getSpanLists(traces); + + assertTrue( + spanLists.anyMatch( + spans -> { + Optional parentSpan = findSpan(spans, parentSpanName); + Optional childSpan = findSpan(spans, childSpanName); + + return parentSpan.isEmpty() && childSpan.isEmpty(); + })); + } + + /** + * Maps the provided traces to a collection of span lists. + * + * @param traces the collection of traces + * @return the collection of span lists within the provided traces + */ + private Stream> getSpanLists(Collection traces) { + return traces.stream() + .flatMap( + trace -> + trace.getResourceSpansList().stream() + .flatMap( + resourceSpans -> + resourceSpans.getScopeSpansList().stream() + .map(ScopeSpans::getSpansList))); + } + + /** + * Tries to find the provided span name in the list of spans. + * + * @param spans the list of spans + * @param spanName the span name to look for + * @return the found span or empty + */ + private Optional findSpan(List spans, String spanName) { + return spans.stream().filter(span -> span.getName().equals(spanName)).findFirst(); + } +} diff --git a/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/utils/LogUtils.java b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/utils/LogUtils.java new file mode 100644 index 0000000..4d1c6af --- /dev/null +++ b/inspectit-gepard-agent/src/test/java/rocks/inspectit/gepard/agent/integrationtest/utils/LogUtils.java @@ -0,0 +1,52 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.agent.integrationtest.utils; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LogUtils { + + /** + * Checks, if the logs contain "HELLO GEPARD" and "BYE GEPARD" for a specific number of times + * + * @param logs the logs + * @param times the amount of times "HELLO GEPARD" and "BYE GEPARD" should be present in the logs + */ + public static void assertLogs(String logs, int times) { + boolean loggedHelloGepard = containsTimes(logs, "HELLO GEPARD", times); + boolean loggedByeGepard = containsTimes(logs, "BYE GEPARD", times); + + assertTrue(loggedHelloGepard); + assertTrue(loggedByeGepard); + } + + /** + * Checks, if a specific message can be found for a specific amount of times inside the provided + * logs. + * + * @return true, if the message appears the expected amount of times in the logs + */ + public static boolean containsTimes(String logs, String message, int times) { + int count = countTimes(logs, message); + return count == times; + } + + /** + * Counts how many times a specific message can be found inside the provided logs + * + * @param logs the logs + * @param message the message to look for + * @return the amount of times the message appears in the logs + */ + public static int countTimes(String logs, String message) { + int count = 0; + int index = 0; + while (index != -1) { + index = logs.indexOf(message, index); + if (index != -1) { + count++; + index += message.length(); + } + } + return count; + } +} diff --git a/inspectit-gepard-bootstrap/README.md b/inspectit-gepard-bootstrap/README.md index 82098cd..2eefb26 100644 --- a/inspectit-gepard-bootstrap/README.md +++ b/inspectit-gepard-bootstrap/README.md @@ -7,3 +7,5 @@ The resulting Jar file of this package will be pushed to the JVM's bootstrap cla This is necessary in order to create a bridge between the user (application) classes and the classes which will be loaded by the OpenTelemetry extension class loader (classes of the inspectIT Gepard Java agent). + +**There should be NO dependencies in this submodule!** diff --git a/inspectit-gepard-bootstrap/build.gradle b/inspectit-gepard-bootstrap/build.gradle index 3bec119..3e9d7c1 100644 --- a/inspectit-gepard-bootstrap/build.gradle +++ b/inspectit-gepard-bootstrap/build.gradle @@ -15,3 +15,5 @@ spotless { licenseHeader '/* (C) 2024 */' } } + +jar.dependsOn(spotlessApply) diff --git a/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/context/InternalInspectitContext.java b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/context/InternalInspectitContext.java new file mode 100644 index 0000000..9fae382 --- /dev/null +++ b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/context/InternalInspectitContext.java @@ -0,0 +1,37 @@ +/* (C) 2024 */ +package rocks.inspectit.gepard.bootstrap.context; + +import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook; + +/** + * This context contains internal data, which will be created while entering an instrumented method + * and can be read while exiting the method. This context should NOT contain any extracted data from + * the application! + */ +public class InternalInspectitContext { + + /** The hook of the current method */ + private final IMethodHook hook; + + /** The created span scope of the current method */ + private final AutoCloseable spanScope; + + public InternalInspectitContext(IMethodHook hook, AutoCloseable spanScope) { + this.spanScope = spanScope; + this.hook = hook; + } + + /** + * @return the current hook + */ + public IMethodHook getHook() { + return hook; + } + + /** + * @return the current span scope + */ + public AutoCloseable getSpanScope() { + return spanScope; + } +} diff --git a/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/IMethodHook.java b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/IMethodHook.java index 8ed8f7e..4a7c84a 100644 --- a/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/IMethodHook.java +++ b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/IMethodHook.java @@ -1,25 +1,37 @@ /* (C) 2024 */ package rocks.inspectit.gepard.bootstrap.instrumentation; -/** */ +import rocks.inspectit.gepard.bootstrap.context.InternalInspectitContext; + +/** + * Defines the behaviour of method hooks. Each hook should contain a method, which will be called + * while entering and exiting an instrumented method. + */ public interface IMethodHook { /** - * Called when the hooked method is entered. + * Called when the hooked method is entered. Stores internal data within the returned context. * * @param instrumentedMethodArgs the arguments passed to the method for which the hook is executed * @param thiz the "this" instance of the invoked method, null if the invoked method is static + * @return the created inspectIT context */ - void onEnter(Object[] instrumentedMethodArgs, Object thiz); + InternalInspectitContext onEnter(Object[] instrumentedMethodArgs, Object thiz); /** * Called when the hooked method exits. * + * @param context the internal inspectIT context of the current method * @param instrumentedMethodArgs the arguments passed to the method for which the hook is executed * @param thiz the "this" instance of the invoked method, null if the invoked method is static * @param returnValue the return value returned by the target method, if this hook is executed at * the end and no exception was thrown * @param thrown the exception thrown by the instrumented method, null otherwise */ - void onExit(Object[] instrumentedMethodArgs, Object thiz, Object returnValue, Throwable thrown); + void onExit( + InternalInspectitContext context, + Object[] instrumentedMethodArgs, + Object thiz, + Object returnValue, + Throwable thrown); } diff --git a/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/noop/NoopMethodHook.java b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/noop/NoopMethodHook.java index fdb860e..cf5bdfb 100644 --- a/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/noop/NoopMethodHook.java +++ b/inspectit-gepard-bootstrap/src/main/java/rocks/inspectit/gepard/bootstrap/instrumentation/noop/NoopMethodHook.java @@ -1,6 +1,7 @@ /* (C) 2024 */ package rocks.inspectit.gepard.bootstrap.instrumentation.noop; +import rocks.inspectit.gepard.bootstrap.context.InternalInspectitContext; import rocks.inspectit.gepard.bootstrap.instrumentation.IMethodHook; /** No-operation implementation of {@link IMethodHook} */ @@ -11,9 +12,15 @@ public class NoopMethodHook implements IMethodHook { private NoopMethodHook() {} @Override - public void onEnter(Object[] instrumentedMethodArgs, Object thiz) {} + public InternalInspectitContext onEnter(Object[] instrumentedMethodArgs, Object thiz) { + return null; + } @Override public void onExit( - Object[] instrumentedMethodArgs, Object thiz, Object returnValue, Throwable thrown) {} + InternalInspectitContext context, + Object[] instrumentedMethodArgs, + Object thiz, + Object returnValue, + Throwable thrown) {} }