LogTransformer xref

View Javadoc

    1   /**
    2    * Copyright (c) 2004-2011 QOS.ch
    3    * All rights reserved.
    4    *
    5    * Permission is hereby granted, free  of charge, to any person obtaining
    6    * a  copy  of this  software  and  associated  documentation files  (the
    7    * "Software"), to  deal in  the Software without  restriction, including
    8    * without limitation  the rights to  use, copy, modify,  merge, publish,
    9    * distribute,  sublicense, and/or sell  copies of  the Software,  and to
    10   * permit persons to whom the Software  is furnished to do so, subject to
    11   * the following conditions:
    12   *
    13   * The  above  copyright  notice  and  this permission  notice  shall  be
    14   * included in all copies or substantial portions of the Software.
    15   *
    16   * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
    17   * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
    18   * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
    19   * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    20   * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    21   * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
    22   * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    23   *
    24   */
    25  /**
    26   *
    27   */
    28  package org.slf4j.instrumentation;
    29
    30  import static org.slf4j.helpers.MessageFormatter.format;
    31
    32  import java.io.ByteArrayInputStream;
    33  import java.lang.instrument.ClassFileTransformer;
    34  import java.security.ProtectionDomain;
    35
    36  import javassist.CannotCompileException;
    37  import javassist.ClassPool;
    38  import javassist.CtBehavior;
    39  import javassist.CtClass;
    40  import javassist.CtField;
    41  import javassist.NotFoundException;
    42
    43  import org.slf4j.helpers.MessageFormatter;
    44
    45  /**
    46   * <p>
    47   * LogTransformer does the work of analyzing each class, and if appropriate add
    48   * log statements to each method to allow logging entry/exit.
    49   * </p>
    50   * <p>
    51   * 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"
    52   * >Add Logging at Class Load Time with Java Instrumentation</a>.
    53   * </p>
    54   */
    55  public class LogTransformer implements ClassFileTransformer {
    56
    57    /**
    58     * Builder provides a flexible way of configuring some of many options on the
    59     * parent class instead of providing many constructors.
    60     *
    61     * {@link http
    62     * ://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html}
    63     *
    64     */
    65    public static class Builder {
    66
    67      /**
    68       * Build and return the LogTransformer corresponding to the options set in
    69       * this Builder.
    70       *
    71       * @return
    72       */
    73      public LogTransformer build() {
    74        if (verbose) {
    75          System.err.println("Creating LogTransformer");
    76        }
    77        return new LogTransformer(this);
    78      }
    79
    80      boolean addEntryExit;
    81
    82      /**
    83       * Should each method log entry (with parameters) and exit (with parameters
    84       * and returnvalue)?
    85       *
    86       * @param b
    87       *          value of flag
    88       * @return
    89       */
    90      public Builder addEntryExit(boolean b) {
    91        addEntryExit = b;
    92        return this;
    93      }
    94
    95      boolean addVariableAssignment;
    96
    97      // private Builder addVariableAssignment(boolean b) {
    98      // System.err.println("cannot currently log variable assignments.");
    99      // addVariableAssignment = b;
    100     // return this;
    101     // }
    102
    103     boolean verbose;
    104
    105     /**
    106      * Should LogTransformer be verbose in what it does? This currently list the
    107      * names of the classes being processed.
    108      *
    109      * @param b
    110      * @return
    111      */
    112     public Builder verbose(boolean b) {
    113       verbose = b;
    114       return this;
    115     }
    116
    117     String[] ignore = { "org/slf4j/", "ch/qos/logback/", "org/apache/log4j/" };
    118
    119     public Builder ignore(String[] strings) {
    120       this.ignore = strings;
    121       return this;
    122     }
    123
    124     private String level = "info";
    125
    126     public Builder level(String level) {
    127       level = level.toLowerCase();
    128       if (level.equals("info") || level.equals("debug")
    129           || level.equals("trace")) {
    130         this.level = level;
    131       } else {
    132         if (verbose) {
    133           System.err.println("level not info/debug/trace : " + level);
    134         }
    135       }
    136       return this;
    137     }
    138   }
    139
    140   private String level;
    141   private String levelEnabled;
    142
    143   private LogTransformer(Builder builder) {
    144     String s = "WARNING: javassist not available on classpath for javaagent, log statements will not be added";
    145     try {
    146       if (Class.forName("javassist.ClassPool") == null) {
    147         System.err.println(s);
    148       }
    149     } catch (ClassNotFoundException e) {
    150       System.err.println(s);
    151     }
    152
    153     this.addEntryExit = builder.addEntryExit;
    154     // this.addVariableAssignment = builder.addVariableAssignment;
    155     this.verbose = builder.verbose;
    156     this.ignore = builder.ignore;
    157     this.level = builder.level;
    158     this.levelEnabled = "is" + builder.level.substring(0, 1).toUpperCase()
    159         + builder.level.substring(1) + "Enabled";
    160   }
    161
    162   private boolean addEntryExit;
    163   // private boolean addVariableAssignment;
    164   private boolean verbose;
    165   private String[] ignore;
    166
    167   public byte[] transform(ClassLoader loader, String className, Class<?> clazz,
    168       ProtectionDomain domain, byte[] bytes) {
    169
    170     try {
    171       return transform0(className, clazz, domain, bytes);
    172     } catch (Exception e) {
    173       System.err.println("Could not instrument " + className);
    174       e.printStackTrace();
    175       return bytes;
    176     }
    177   }
    178
    179   /**
    180    * transform0 sees if the className starts with any of the namespaces to
    181    * ignore, if so it is returned unchanged. Otherwise it is processed by
    182    * doClass(...)
    183    *
    184    * @param className
    185    * @param clazz
    186    * @param domain
    187    * @param bytes
    188    * @return
    189    */
    190
    191   private byte[] transform0(String className, Class<?> clazz,
    192       ProtectionDomain domain, byte[] bytes) {
    193
    194     try {
    195       for (int i = 0; i < ignore.length; i++) {
    196         if (className.startsWith(ignore[i])) {
    197           return bytes;
    198         }
    199       }
    200       String slf4jName = "org.slf4j.LoggerFactory";
    201       try {
    202         if (domain != null && domain.getClassLoader() != null) {
    203           domain.getClassLoader().loadClass(slf4jName);
    204         } else {
    205           if (verbose) {
    206             System.err.println("Skipping " + className
    207                 + " as it doesn't have a domain or a class loader.");
    208           }
    209           return bytes;
    210         }
    211       } catch (ClassNotFoundException e) {
    212         if (verbose) {
    213           System.err.println("Skipping " + className
    214               + " as slf4j is not available to it");
    215         }
    216         return bytes;
    217       }
    218       if (verbose) {
    219         System.err.println("Processing " + className);
    220       }
    221       return doClass(className, clazz, bytes);
    222     } catch (Throwable e) {
    223       System.out.println("e = " + e);
    224       return bytes;
    225     }
    226   }
    227
    228   private String loggerName;
    229
    230   /**
    231    * doClass() process a single class by first creates a class description from
    232    * the byte codes. If it is a class (i.e. not an interface) the methods
    233    * defined have bodies, and a static final logger object is added with the
    234    * name of this class as an argument, and each method then gets processed with
    235    * doMethod(...) to have logger calls added.
    236    *
    237    * @param name
    238    *          class name (slashes separate, not dots)
    239    * @param clazz
    240    * @param b
    241    * @return
    242    */
    243   private byte[] doClass(String name, Class<?> clazz, byte[] b) {
    244     ClassPool pool = ClassPool.getDefault();
    245     CtClass cl = null;
    246     try {
    247       cl = pool.makeClass(new ByteArrayInputStream(b));
    248       if (cl.isInterface() == false) {
    249
    250         loggerName = "_____log";
    251
    252         // We have to declare the log variable.
    253
    254         String pattern1 = "private static org.slf4j.Logger {};";
    255         String loggerDefinition = format(pattern1, loggerName).getMessage();
    256         CtField field = CtField.make(loggerDefinition, cl);
    257
    258         // and assign it the appropriate value.
    259
    260         String pattern2 = "org.slf4j.LoggerFactory.getLogger({}.class);";
    261         String replace = name.replace('/', '.');
    262         String getLogger = format(pattern2, replace).getMessage();
    263
    264         cl.addField(field, getLogger);
    265
    266         // then check every behaviour (which includes methods). We are
    267         // only
    268         // interested in non-empty ones, as they have code.
    269         // NOTE: This will be changed, as empty methods should be
    270         // instrumented too.
    271
    272         CtBehavior[] methods = cl.getDeclaredBehaviors();
    273         for (int i = 0; i < methods.length; i++) {
    274           if (methods[i].isEmpty() == false) {
    275             doMethod(methods[i]);
    276           }
    277         }
    278         b = cl.toBytecode();
    279       }
    280     } catch (Exception e) {
    281       System.err.println("Could not instrument " + name + ", " + e);
    282       e.printStackTrace(System.err);
    283     } finally {
    284       if (cl != null) {
    285         cl.detach();
    286       }
    287     }
    288     return b;
    289   }
    290
    291   /**
    292    * process a single method - this means add entry/exit logging if requested.
    293    * It is only called for methods with a body.
    294    *
    295    * @param method
    296    *          method to work on
    297    * @throws NotFoundException
    298    * @throws CannotCompileException
    299    */
    300   private void doMethod(CtBehavior method) throws NotFoundException,
    301       CannotCompileException {
    302
    303     String signature = JavassistHelper.getSignature(method);
    304     String returnValue = JavassistHelper.returnValue(method);
    305
    306     if (addEntryExit) {
    307       String messagePattern = "if ({}.{}()) {}.{}(\">> {}\");";
    308       Object[] arg1 = new Object[] { loggerName, levelEnabled, loggerName,
    309           level, signature };
    310       String before = MessageFormatter.arrayFormat(messagePattern, arg1)
    311           .getMessage();
    312       // System.out.println(before);
    313       method.insertBefore(before);
    314
    315       String messagePattern2 = "if ({}.{}()) {}.{}(\"<< {}{}\");";
    316       Object[] arg2 = new Object[] { loggerName, levelEnabled, loggerName,
    317           level, signature, returnValue };
    318       String after = MessageFormatter.arrayFormat(messagePattern2, arg2)
    319           .getMessage();
    320       // System.out.println(after);
    321       method.insertAfter(after);
    322     }
    323   }
    324 }