/** * */ package org.slf4j.instrumentation; import static org.slf4j.helpers.MessageFormatter.format; import java.io.ByteArrayInputStream; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtBehavior; import javassist.CtClass; import javassist.CtField; import javassist.NotFoundException; import org.slf4j.helpers.MessageFormatter; /** * <p> * LogTransformer does the work of analyzing each class, and if appropriate add * log statements to each method to allow logging entry/exit. * </p> * <p> * This class is based on the article <a href="http://today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html" * >Add Logging at Class Load Time with Java Instrumentation</a>. * </p> */ public class LogTransformer implements ClassFileTransformer { /** * Builder provides a flexible way of configuring some of many options on the * parent class instead of providing many constructors. * * {@link http * ://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html} * */ public static class Builder { /** * Build and return the LogTransformer corresponding to the options set in * this Builder. * * @return */ public LogTransformer build() { if (verbose) { System.err.println("Creating LogTransformer"); } return new LogTransformer(this); } boolean addEntryExit; /** * Should each method log entry (with parameters) and exit (with parameters * and returnvalue)? * * @param b * value of flag * @return */ public Builder addEntryExit(boolean b) { addEntryExit = b; return this; } boolean addVariableAssignment; // private Builder addVariableAssignment(boolean b) { // System.err.println("cannot currently log variable assignments."); // addVariableAssignment = b; // return this; // } boolean verbose; /** * Should LogTransformer be verbose in what it does? This currently list the * names of the classes being processed. * * @param b * @return */ public Builder verbose(boolean b) { verbose = b; return this; } String[] ignore = { "org/slf4j/", "ch/qos/logback/", "org/apache/log4j/" }; public Builder ignore(String[] strings) { this.ignore = strings; return this; } private String level = "info"; public Builder level(String level) { level = level.toLowerCase(); if (level.equals("info") || level.equals("debug") || level.equals("trace")) { this.level = level; } else { if (verbose) { System.err.println("level not info/debug/trace : " + level); } } return this; } } private String level; private String levelEnabled; private LogTransformer(Builder builder) { String s = "WARNING: javassist not available on classpath for javaagent, log statements will not be added"; try { if (Class.forName("javassist.ClassPool") == null) { System.err.println(s); } } catch (ClassNotFoundException e) { System.err.println(s); } this.addEntryExit = builder.addEntryExit; // this.addVariableAssignment = builder.addVariableAssignment; this.verbose = builder.verbose; this.ignore = builder.ignore; this.level = builder.level; this.levelEnabled = "is" + builder.level.substring(0, 1).toUpperCase() + builder.level.substring(1) + "Enabled"; } private boolean addEntryExit; // private boolean addVariableAssignment; private boolean verbose; private String[] ignore; public byte[] transform(ClassLoader loader, String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) { try { return transform0(className, clazz, domain, bytes); } catch (Exception e) { System.err.println("Could not instrument " + className); e.printStackTrace(); return bytes; } } /** * transform0 sees if the className starts with any of the namespaces to * ignore, if so it is returned unchanged. Otherwise it is processed by * doClass(...) * * @param className * @param clazz * @param domain * @param bytes * @return */ private byte[] transform0(String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) { try { for (int i = 0; i < ignore.length; i++) { if (className.startsWith(ignore[i])) { return bytes; } } String slf4jName = "org.slf4j.LoggerFactory"; try { if (domain != null && domain.getClassLoader() != null) { domain.getClassLoader().loadClass(slf4jName); } else { if (verbose) { System.err.println("Skipping " + className + " as it doesn't have a domain or a class loader."); } return bytes; } } catch (ClassNotFoundException e) { if (verbose) { System.err.println("Skipping " + className + " as slf4j is not available to it"); } return bytes; } if (verbose) { System.err.println("Processing " + className); } return doClass(className, clazz, bytes); } catch (Throwable e) { System.out.println("e = " + e); return bytes; } } private String loggerName; /** * doClass() process a single class by first creates a class description from * the byte codes. If it is a class (i.e. not an interface) the methods * defined have bodies, and a static final logger object is added with the * name of this class as an argument, and each method then gets processed with * doMethod(...) to have logger calls added. * * @param name * class name (slashes separate, not dots) * @param clazz * @param b * @return */ private byte[] doClass(String name, Class<?> clazz, byte[] b) { ClassPool pool = ClassPool.getDefault(); CtClass cl = null; try { cl = pool.makeClass(new ByteArrayInputStream(b)); if (cl.isInterface() == false) { loggerName = "_____log"; // We have to declare the log variable. String pattern1 = "private static org.slf4j.Logger {};"; String loggerDefinition = format(pattern1, loggerName).getMessage(); CtField field = CtField.make(loggerDefinition, cl); // and assign it the appropriate value. String pattern2 = "org.slf4j.LoggerFactory.getLogger({}.class);"; String replace = name.replace('/', '.'); String getLogger = format(pattern2, replace).getMessage(); cl.addField(field, getLogger); // then check every behaviour (which includes methods). We are // only // interested in non-empty ones, as they have code. // NOTE: This will be changed, as empty methods should be // instrumented too. CtBehavior[] methods = cl.getDeclaredBehaviors(); for (int i = 0; i < methods.length; i++) { if (methods[i].isEmpty() == false) { doMethod(methods[i]); } } b = cl.toBytecode(); } } catch (Exception e) { System.err.println("Could not instrument " + name + ", " + e); e.printStackTrace(System.err); } finally { if (cl != null) { cl.detach(); } } return b; } /** * process a single method - this means add entry/exit logging if requested. * It is only called for methods with a body. * * @param method * method to work on * @throws NotFoundException * @throws CannotCompileException */ private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException { String signature = JavassistHelper.getSignature(method); String returnValue = JavassistHelper.returnValue(method); if (addEntryExit) { String messagePattern = "if ({}.{}()) {}.{}(\">> {}\");"; Object[] arg1 = new Object[] { loggerName, levelEnabled, loggerName, level, signature }; String before = MessageFormatter.arrayFormat(messagePattern, arg1) .getMessage(); // System.out.println(before); method.insertBefore(before); String messagePattern2 = "if ({}.{}()) {}.{}(\"<< {}{}\");"; Object[] arg2 = new Object[] { loggerName, levelEnabled, loggerName, level, signature, returnValue }; String after = MessageFormatter.arrayFormat(messagePattern2, arg2) .getMessage(); // System.out.println(after); method.insertAfter(after); } } }