Skip to content

Commit

Permalink
Feature/custom tracing (#20)
Browse files Browse the repository at this point in the history
* refactor: add method hook state

* change slf4j dependency

* add README note

* test: add method hook tests

* apply spotless

* feature: record spans in method hooks (wip)

* refactor: record custom spans with action (wip)

* append bootstrap README

* apply spotless

* refactor: sonarcloud issues

* add tests

* little refactor

* fix test

* refactor tests

* add tracing integration test

* minor fixes
  • Loading branch information
EddeCCC authored Oct 10, 2024
1 parent 6051bfe commit fe4461a
Show file tree
Hide file tree
Showing 28 changed files with 834 additions and 243 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 2 additions & 2 deletions inspectit-gepard-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -78,7 +79,8 @@ public int updateHooks(Class<?> clazz, ClassHookConfiguration classConfiguration
String signature = getSignature(method);
Optional<MethodHook> maybeHook = getCurrentHook(clazz, signature);
if (maybeHook.isEmpty()) {
setHook(clazz, signature, new MethodHook());
MethodHook hook = MethodHookGenerator.createHook(method);
setHook(clazz, signature, hook);
operationCounter.addAndGet(1);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading

0 comments on commit fe4461a

Please sign in to comment.