Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Javassist to log method calls and argument values, how to make a logger class visible in every instrumented class?

The tool (in this repo) comprises 3 classes (given below). The problem is how to make my ParaTracer.Logger class visible in every class I instrument (such as java.util.Random shown below). The statement cp.importPackage( "ParaTracer.Logger"); doesn't seem to work and I am getting this error:

java.lang.NoClassDefFoundError: ParaTracer/Logger at java.util.Random.nextLong(Random.java)

I tried dynamically loading the Logger class inside every instrumented class. But it seems I was using Class.getMethod() incorrectly, or the Javassist compiler is too primitive to compile dynamic class-loading code. I get this error:

javassist.CannotCompileException: [source error] getMethod(java.lang.String,java.lang.Class,java.lang.Class) not found in java.lang.Class

The following 3 classes are exported into a JAR file with MANIFEST.MF file defining the Premain-Class and is passed to the JVM when running any instrumented program using the switch:

-javaagent:/Path/To/ParaTracerAgent.jar

Here are the 3 classes.

package ParaTracer;

import java.lang.instrument.Instrumentation;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;

public class ParaTracer {

    private static volatile Instrumentation instr;

    public static void premain(String agentArgs, Instrumentation inst) {
        instr = inst;
        SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer( transformer, false );
    }
}

The transformer class:

package ParaTracer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.HashSet;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;


public class SimpleClassTransformer implements ClassFileTransformer {

    public HashMap< String, HashSet< String > > mInstrumentedMethods;

    public SimpleClassTransformer() {
        mInstrumentedMethods = new HashMap< String, HashSet< String > >();

        mInstrumentedMethods.put( "java.util.Random", new HashSet< String >() );
        mInstrumentedMethods.get( "java.util.Random").add( "nextLong" );
    }

    @Override
    public byte[] transform(
        ClassLoader       loader,
        String            className,
        Class<?>          classBeingRedefined,
        ProtectionDomain  protectionDomain,
        byte[]            classfileBuffer) throws IllegalClassFormatException {

        System.err.println( "---- Instrumenting: " + className );

        byte[] byteCode = classfileBuffer;

        String normalizedClassName = className.replaceAll("/", ".");

        if ( mInstrumentedMethods.containsKey( normalizedClassName ) ) {
            try {
                ClassPool cp = ClassPool.getDefault();

                cp.importPackage( "ParaTracer.Logger");

                CtClass cc = cp.get( normalizedClassName );

                for( String method : mInstrumentedMethods.get( normalizedClassName ) ) {
                    CtMethod  m  = cc.getDeclaredMethod( method );

                    StringBuilder sbs = new StringBuilder();
                    sbs.append( "long tid = Thread.currentThread().getId();" );
                    sbs.append( "StringBuilder sbArgs = new StringBuilder();" );
                    sbs.append( "sbArgs.append( System.identityHashCode( $0 ) );" );
                    CtClass[] pTypes = m.getParameterTypes();
                    for( int i=0; i < pTypes.length; ++i ) {
                        CtClass pType = pTypes[i];
                        if ( pType.isPrimitive() ) {
                            sbs.append( "sbArgs.append( \", \" + $args[" + i + "] );" );
                        } else {
                            sbs.append( "sbArgs.append( \", \" + System.identityHashCode( $args[" + i + "] ) );" );
                        }
                    }
                    sbs.append( "ParaTracer.Logger.pushArgs( tid, sbArgs.toString() );" );
                    sbs.append( "StringBuilder sb = new StringBuilder();" );
                    sbs.append( "sb.append( tid + \" : " + m.getLongName() + ".<START>(\" );" );
                    sbs.append( "sb.append( sbArgs.toString() );" );
                    sbs.append( "sb.append( \")\" );" );
                    sbs.append( "ParaTracer.Logger.print( sb.toString() );" );

                    m.insertBefore("{" + sbs.toString() + "}");

                    StringBuilder sbe = new StringBuilder();
                    sbe.append( "long tid = Thread.currentThread().getId();" );
                    sbe.append( "String args = ParaTracer.Logger.popArgs( tid );" );
                    sbe.append( "StringBuilder sb = new StringBuilder();" );
                    sbe.append( "sb.append( tid + \" : " + m.getLongName() + ".<END>(\" );" );
                    sbe.append( "sb.append( args );" );
                    sbe.append( "sb.append( \")\" );" );
                    sbe.append( "ParaTracer.Logger.print( sb.toString() );" );

                    m.insertAfter("{" + sbe.toString() + "}");
                }
                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return byteCode;
    }
}

The thread-safe logger class is given by:

package ParaTracer;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Stack;

public class Logger {

    private static String loggerFilePath = "\"/some/fixed/path\"";
    private static FileWriter   fw;
    private static PrintWriter  out;
    private static HashMap< Long, Stack<String> > callStacks;

    public static synchronized void pushArgs( long tid, String args ) {
        try {
            init();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if ( ! callStacks.containsKey( tid ) ) {
            callStacks.put( tid, new Stack<String>() );
        }
        callStacks.get( tid ).push( args );
    }

    public static synchronized String popArgs( long tid ) {
        assert( callStacks.containsKey( tid ) );
        assert( ! callStacks.get( tid ).empty() );
        return callStacks.get( tid ).pop();
    }

    public static synchronized void shutdown() {
        if ( fw == null ) return;
        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void print( String str ) {
        try {
            init();
        } catch (IOException e) {
            e.printStackTrace();
        }
        out.print( str );
    }

    private static void init() throws IOException {
        if ( fw != null ) return;
        fw  = new FileWriter( loggerFilePath );
        out = new PrintWriter( fw );
        callStacks = new HashMap< Long, Stack<String> >();
    }
}
like image 422
Ahmed Nassar Avatar asked Mar 16 '23 12:03

Ahmed Nassar


1 Answers

According to the documentation for Java agents the agent class is loaded by the system classloader. But if you want to instrument core Java classes and refer from those to a custom class of your own then that class would need to be available to the bootstrap classloader rather than the system one.

Move your Logger class into a separate JAR file, and list that file in the Boot-Class-Path attribute of the agent JAR's manifest:

Boot-Class-Path: ParaTracerLogger.jar

Now the logger class is visible on the bootstrap loader and can be seen by the instrumented java.lang classes.

like image 135
Ian Roberts Avatar answered Mar 19 '23 00:03

Ian Roberts