Skip to content

Commit db459a4

Browse files
committed
refactor(agent): Refactor Hook.apply and ClassFileTransformer
Refactor `Hook.apply` to return a `Set<ApplicationAction>` indicating whether the method was marked for ByteBuddy instrumentation or instrumented by Javassist. This allows `ClassFileTransformer` to conditionally apply the `AppMapInstrumented` annotation only when ByteBuddy instrumentation is actually needed. Additionally, add improved logging for Javassist instrumentation failures and guard against excessive logging of these exceptions.
1 parent 9f75dc2 commit db459a4

2 files changed

Lines changed: 109 additions & 88 deletions

File tree

agent/src/main/java/com/appland/appmap/transform/ClassFileTransformer.java

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -189,47 +189,24 @@ private void processClass(CtClass ctClass) {
189189
}
190190
}
191191

192-
private boolean applyHooks(CtBehavior behavior) {
193-
boolean traceClass = tracePrefix == null || behavior.getDeclaringClass().getName().startsWith(tracePrefix);
194-
192+
private Set<Hook.ApplicationAction> applyHooks(CtBehavior behavior, boolean traceClass) {
195193
try {
196194
List<HookSite> hookSites = getHookSites(behavior);
197195

198196
if (hookSites == null) {
199197
if (traceClass) {
200198
logger.trace("no hook sites");
201199
}
202-
return false;
200+
return java.util.Collections.emptySet();
203201
}
204202

205-
Hook.apply(behavior, hookSites);
206-
207-
if (logger.isDebugEnabled()) {
208-
for (HookSite hookSite : hookSites) {
209-
final Hook hook = hookSite.getHook();
210-
String className = behavior.getDeclaringClass().getName();
211-
if (tracePrefix != null && !className.startsWith(tracePrefix)) {
212-
continue;
213-
}
214-
215-
if (traceClass) {
216-
logger.trace("hooked {}.{}{} on ({},{}) with {}",
217-
className,
218-
behavior.getName(),
219-
behavior.getMethodInfo().getDescriptor(),
220-
hook.getMethodEvent().getEventString(),
221-
hook.getPosition(),
222-
hook);
223-
}
224-
}
225-
}
226-
return true;
203+
return Hook.apply(behavior, hookSites, traceClass);
227204

228205
} catch (NoSourceAvailableException e) {
229206
Logger.println(e);
230207
}
231208

232-
return false;
209+
return java.util.Collections.emptySet();
233210
}
234211

235212
public List<HookSite> getHookSites(CtBehavior behavior) {
@@ -292,7 +269,7 @@ public byte[] transform(ClassLoader loader,
292269
try {
293270
ClassPool classPool = AppMapClassPool.get();
294271
if (traceClass) {
295-
logger.debug("className: {}", className);
272+
logger.trace("className: {}", className);
296273
}
297274

298275
ctClass = classPool.makeClass(new ByteArrayInputStream(bytes));
@@ -317,7 +294,8 @@ public byte[] transform(ClassLoader loader,
317294
return null;
318295
}
319296

320-
boolean hookApplied = false;
297+
boolean bytecodeModified = false;
298+
boolean needsByteBuddy = false;
321299
for (CtBehavior behavior : ctClass.getDeclaredBehaviors()) {
322300
if (traceClass) {
323301
logger.trace("behavior: {}", behavior.getLongName());
@@ -331,24 +309,27 @@ public byte[] transform(ClassLoader loader,
331309
}
332310

333311
methodsExamined++;
334-
if (this.applyHooks(behavior)) {
335-
hookApplied = true;
312+
Set<Hook.ApplicationAction> actions = this.applyHooks(behavior, traceClass);
313+
if (!actions.isEmpty()) {
314+
bytecodeModified = true;
336315
methodsHooked++;
316+
if (actions.contains(Hook.ApplicationAction.MARKED)) {
317+
needsByteBuddy = true;
318+
}
337319
}
338320
}
339321

340-
if (hookApplied) {
341-
// One or more of the methods in the the class were marked as needing to
322+
if (bytecodeModified) {
323+
// One or more of the methods in the class were marked as needing to
342324
// be instrumented. Mark the class so the bytebuddy transformer will
343325
// know it needs to be instrumented.
344-
ClassFile classFile = ctClass.getClassFile();
345-
ConstPool constPool = classFile.getConstPool();
346-
Annotation annot = new Annotation(AppMapInstrumented.class.getName(), constPool);
347-
AnnotationUtil.setAnnotation(new AnnotatedClass(ctClass), annot);
348-
349-
if (traceClass) {
350-
logger.trace("hooks applied to {}", className);
326+
if (needsByteBuddy) {
327+
ClassFile classFile = ctClass.getClassFile();
328+
ConstPool constPool = classFile.getConstPool();
329+
Annotation annot = new Annotation(AppMapInstrumented.class.getName(), constPool);
330+
AnnotationUtil.setAnnotation(new AnnotatedClass(ctClass), annot);
351331
}
332+
352333
if (logger.isDebugEnabled()) {
353334
packagesHooked.compute(ctClass.getPackageName(), (k, v) -> v == null ? 1 : v + 1);
354335
}

agent/src/main/java/com/appland/appmap/transform/annotations/Hook.java

Lines changed: 88 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Arrays;
44
import java.util.Comparator;
5+
import java.util.EnumSet;
56
import java.util.HashSet;
67
import java.util.List;
78
import java.util.Map;
@@ -107,25 +108,72 @@ public HookSite prepare(CtBehavior targetBehavior, Map<String, Object> hookConte
107108
return new HookSite(this, behaviorOrdinal, binding);
108109
}
109110

110-
public static void apply(CtBehavior targetBehavior, List<HookSite> hookSites) {
111+
public enum ApplicationAction {
112+
MARKED,
113+
INSTRUMENTED
114+
}
115+
116+
// We only log the first exception to avoid flooding the logs at the debug level.
117+
// Note this variable is not thread safe, but this is okay; the worst that can happen is
118+
// that we log more than one exception in a multi-threaded scenario.
119+
private static boolean exceptionLogged = false;
120+
121+
public static Set<ApplicationAction> apply(CtBehavior targetBehavior, List<HookSite> hookSites, boolean traceClass) {
122+
Set<ApplicationAction> actions = EnumSet.noneOf(ApplicationAction.class);
111123
MethodInfo methodInfo = targetBehavior.getMethodInfo();
112124
AnnotationsAttribute attr =
113125
(AnnotationsAttribute)methodInfo.getAttribute(AnnotationsAttribute.visibleTag);
114126

115-
// If the behavior is marked as an app method, update the annotation with
116-
// the behavior ordinals so the bytebuddy transformer can instrument it.
117-
if (attr.getAnnotation(AppMapAppMethod.class.getName()) != null) {
118-
setBehaviorOrdinals(targetBehavior, hookSites);
119-
}
127+
if (attr != null) {
128+
// If the behavior is marked as an app method, update the annotation with
129+
// the behavior ordinals so the bytebuddy transformer can instrument it.
130+
if (attr.getAnnotation(AppMapAppMethod.class.getName()) != null) {
131+
setBehaviorOrdinals(targetBehavior, hookSites);
132+
actions.add(ApplicationAction.MARKED);
133+
if (traceClass) {
134+
logger.debug("tracing {}.{}{}",
135+
targetBehavior.getDeclaringClass().getName(),
136+
targetBehavior.getName(),
137+
targetBehavior.getMethodInfo().getDescriptor());
138+
}
139+
}
120140

121-
// If it's (also) marked as an agent method, it needs to be instrumented
122-
// by javassist.
123-
if (attr.getAnnotation(AppMapAgentMethod.class.getName()) != null) {
124-
instrument(targetBehavior, hookSites);
141+
// If it's (also) marked as an agent method, it needs to be instrumented
142+
// by javassist.
143+
if (attr.getAnnotation(AppMapAgentMethod.class.getName()) != null) {
144+
try {
145+
instrument(targetBehavior, hookSites);
146+
actions.add(ApplicationAction.INSTRUMENTED);
147+
if (traceClass) {
148+
String hooks = hookSites.stream()
149+
.map(h -> h.getHook().toString())
150+
.collect(Collectors.joining(", "));
151+
logger.debug("{}.{}{} instrumented with hooks: {}",
152+
targetBehavior.getDeclaringClass().getName(),
153+
targetBehavior.getName(),
154+
targetBehavior.getMethodInfo().getDescriptor(),
155+
hooks);
156+
}
157+
} catch (CannotCompileException | NotFoundException e) {
158+
String msg = String.format("failed to instrument %s.%s: %s",
159+
targetBehavior.getDeclaringClass().getName(), targetBehavior.getName(), e.getMessage());
160+
if (!exceptionLogged) {
161+
logger.debug(e, msg);
162+
exceptionLogged = true;
163+
} else {
164+
// Log at trace level after the first one to avoid flooding the debug logs
165+
logger.trace(e, msg);
166+
logger.debug(msg);
167+
}
168+
}
169+
}
125170
}
171+
172+
return actions;
126173
}
127174

128-
public static void instrument(CtBehavior targetBehavior, List<HookSite> hookSites) {
175+
public static void instrument(CtBehavior targetBehavior, List<HookSite> hookSites)
176+
throws CannotCompileException, NotFoundException {
129177
final CtClass returnType = getReturnType(targetBehavior);
130178
final Boolean returnsVoid = (returnType == CtClass.voidType);
131179

@@ -150,44 +198,36 @@ public static void instrument(CtBehavior targetBehavior, List<HookSite> hookSite
150198

151199
}
152200

153-
try {
154-
String beforeSrcBlock = beforeSrcBlock(uniqueLocks.toString(),
155-
invocations[MethodEvent.METHOD_INVOCATION.getIndex()]);
156-
logger.trace("{}: beforeSrcBlock:\n{}", targetBehavior::getName, beforeSrcBlock::toString);
157-
targetBehavior.insertBefore(
158-
beforeSrcBlock);
159-
160-
String afterSrcBlock = afterSrcBlock(invocations[MethodEvent.METHOD_RETURN.getIndex()]);
161-
logger.trace("{}: afterSrcBlock:\n{}", targetBehavior::getName, afterSrcBlock::toString);
162-
163-
targetBehavior.insertAfter(
164-
afterSrcBlock);
165-
166-
ClassPool cp = AppMapClassPool.get();
167-
String exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return;}";
168-
if (returnsVoid) {
169-
targetBehavior.addCatch(exitEarlyCatchSrc,
170-
cp.get("com.appland.appmap.process.ExitEarly"));
171-
} else if (!returnType.isPrimitive()) {
172-
exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return("
173-
+ returnType.getName() + ")$e.getReturnValue();}";
174-
targetBehavior
175-
.addCatch(exitEarlyCatchSrc, cp.get("com.appland.appmap.process.ExitEarly"));
176-
}
177-
logger.trace("{}: catch1Src:\n{}", targetBehavior::getName, exitEarlyCatchSrc::toString);
178-
179-
String catchSrcBlock = catchSrcBlock(invocations[MethodEvent.METHOD_EXCEPTION.getIndex()]);
180-
targetBehavior.addCatch(
181-
catchSrcBlock,
182-
cp.get("java.lang.Throwable"));
183-
logger.trace("{}: catchSrcBlock:\n{}", targetBehavior::getName, catchSrcBlock::toString);
184-
185-
} catch (CannotCompileException e) {
186-
logger.debug(e, "failed to compile {}.{}", targetBehavior.getDeclaringClass().getName(),
187-
targetBehavior.getName());
188-
} catch (NotFoundException e) {
189-
logger.debug(e);
201+
String beforeSrcBlock = beforeSrcBlock(uniqueLocks.toString(),
202+
invocations[MethodEvent.METHOD_INVOCATION.getIndex()]);
203+
logger.trace("{}: beforeSrcBlock:\n{}", targetBehavior::getName, beforeSrcBlock::toString);
204+
targetBehavior.insertBefore(
205+
beforeSrcBlock);
206+
207+
String afterSrcBlock = afterSrcBlock(invocations[MethodEvent.METHOD_RETURN.getIndex()]);
208+
logger.trace("{}: afterSrcBlock:\n{}", targetBehavior::getName, afterSrcBlock::toString);
209+
210+
targetBehavior.insertAfter(
211+
afterSrcBlock);
212+
213+
ClassPool cp = AppMapClassPool.get();
214+
String exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return;}";
215+
if (returnsVoid) {
216+
targetBehavior.addCatch(exitEarlyCatchSrc,
217+
cp.get("com.appland.appmap.process.ExitEarly"));
218+
} else if (!returnType.isPrimitive()) {
219+
exitEarlyCatchSrc = "{com.appland.appmap.process.ThreadLock.current().exit();return("
220+
+ returnType.getName() + ")$e.getReturnValue();}";
221+
targetBehavior
222+
.addCatch(exitEarlyCatchSrc, cp.get("com.appland.appmap.process.ExitEarly"));
190223
}
224+
logger.trace("{}: catch1Src:\n{}", targetBehavior::getName, exitEarlyCatchSrc::toString);
225+
226+
String catchSrcBlock = catchSrcBlock(invocations[MethodEvent.METHOD_EXCEPTION.getIndex()]);
227+
targetBehavior.addCatch(
228+
catchSrcBlock,
229+
cp.get("java.lang.Throwable"));
230+
logger.trace("{}: catchSrcBlock:\n{}", targetBehavior::getName, catchSrcBlock::toString);
191231
}
192232

193233
private static void setBehaviorOrdinals(CtBehavior behavior,

0 commit comments

Comments
 (0)