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       * <a href="http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html">http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html</a>
    62       *
    63       */
    64      public static class Builder {
    65
    66          /**
    67           * Build and return the LogTransformer corresponding to the options set in
    68           * this Builder.
    69           *
    70           * @return
    71           */
    72          public LogTransformer build() {
    73              if (verbose) {
    74                  System.err.println("Creating LogTransformer");
    75              }
    76              return new LogTransformer(this);
    77          }
    78
    79          boolean addEntryExit;
    80
    81          /**
    82           * Should each method log entry (with parameters) and exit (with parameters
    83           * and return value)?
    84           *
    85           * @param b
    86           *          value of flag
    87           * @return
    88           */
    89          public Builder addEntryExit(boolean b) {
    90              addEntryExit = b;
    91              return this;
    92          }
    93
    94          boolean addVariableAssignment;
    95
    96          // private Builder addVariableAssignment(boolean b) {
    97          // System.err.println("cannot currently log variable assignments.");
    98          // addVariableAssignment = b;
    99          // return this;
    100         // }
    101
    102         boolean verbose;
    103
    104         /**
    105          * Should LogTransformer be verbose in what it does? This currently list the
    106          * names of the classes being processed.
    107          *
    108          * @param b
    109          * @return
    110          */
    111         public Builder verbose(boolean b) {
    112             verbose = b;
    113             return this;
    114         }
    115
    116         String[] ignore = { "org/slf4j/", "ch/qos/logback/", "org/apache/log4j/" };
    117
    118         public Builder ignore(String[] strings) {
    119             this.ignore = strings;
    120             return this;
    121         }
    122
    123         private String level = "info";
    124
    125         public Builder level(String level) {
    126             level = level.toLowerCase();
    127             if (level.equals("info") || level.equals("debug") || level.equals("trace")) {
    128                 this.level = level;
    129             } else {
    130                 if (verbose) {
    131                     System.err.println("level not info/debug/trace : " + level);
    132                 }
    133             }
    134             return this;
    135         }
    136     }
    137
    138     private String level;
    139     private String levelEnabled;
    140
    141     private LogTransformer(Builder builder) {
    142         String s = "WARNING: javassist not available on classpath for javaagent, log statements will not be added";
    143         try {
    144             if (Class.forName("javassist.ClassPool") == null) {
    145                 System.err.println(s);
    146             }
    147         } catch (ClassNotFoundException e) {
    148             System.err.println(s);
    149         }
    150
    151         this.addEntryExit = builder.addEntryExit;
    152         // this.addVariableAssignment = builder.addVariableAssignment;
    153         this.verbose = builder.verbose;
    154         this.ignore = builder.ignore;
    155         this.level = builder.level;
    156         this.levelEnabled = "is" + builder.level.substring(0, 1).toUpperCase() + builder.level.substring(1) + "Enabled";
    157     }
    158
    159     private boolean addEntryExit;
    160     // private boolean addVariableAssignment;
    161     private boolean verbose;
    162     private String[] ignore;
    163
    164     public byte[] transform(ClassLoader loader, String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) {
    165
    166         try {
    167             return transform0(className, clazz, domain, bytes);
    168         } catch (Exception e) {
    169             System.err.println("Could not instrument " + className);
    170             e.printStackTrace();
    171             return bytes;
    172         }
    173     }
    174
    175     /**
    176      * transform0 sees if the className starts with any of the namespaces to
    177      * ignore, if so it is returned unchanged. Otherwise it is processed by
    178      * doClass(...)
    179      *
    180      * @param className
    181      * @param clazz
    182      * @param domain
    183      * @param bytes
    184      * @return
    185      */
    186
    187     private byte[] transform0(String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) {
    188
    189         try {
    190             for (int i = 0; i < ignore.length; i++) {
    191                 if (className.startsWith(ignore[i])) {
    192                     return bytes;
    193                 }
    194             }
    195             String slf4jName = "org.slf4j.LoggerFactory";
    196             try {
    197                 if (domain != null && domain.getClassLoader() != null) {
    198                     domain.getClassLoader().loadClass(slf4jName);
    199                 } else {
    200                     if (verbose) {
    201                         System.err.println("Skipping " + className + " as it doesn't have a domain or a class loader.");
    202                     }
    203                     return bytes;
    204                 }
    205             } catch (ClassNotFoundException e) {
    206                 if (verbose) {
    207                     System.err.println("Skipping " + className + " as slf4j is not available to it");
    208                 }
    209                 return bytes;
    210             }
    211             if (verbose) {
    212                 System.err.println("Processing " + className);
    213             }
    214             return doClass(className, clazz, bytes);
    215         } catch (Throwable e) {
    216             System.out.println("e = " + e);
    217             return bytes;
    218         }
    219     }
    220
    221     private String loggerName;
    222
    223     /**
    224      * doClass() process a single class by first creates a class description from
    225      * the byte codes. If it is a class (i.e. not an interface) the methods
    226      * defined have bodies, and a static final logger object is added with the
    227      * name of this class as an argument, and each method then gets processed with
    228      * doMethod(...) to have logger calls added.
    229      *
    230      * @param name
    231      *          class name (slashes separate, not dots)
    232      * @param clazz
    233      * @param b
    234      * @return
    235      */
    236     private byte[] doClass(String name, Class<?> clazz, byte[] b) {
    237         ClassPool pool = ClassPool.getDefault();
    238         CtClass cl = null;
    239         try {
    240             cl = pool.makeClass(new ByteArrayInputStream(b));
    241             if (cl.isInterface() == false) {
    242
    243                 loggerName = "_____log";
    244
    245                 // We have to declare the log variable.
    246
    247                 String pattern1 = "private static org.slf4j.Logger {};";
    248                 String loggerDefinition = format(pattern1, loggerName).getMessage();
    249                 CtField field = CtField.make(loggerDefinition, cl);
    250
    251                 // and assign it the appropriate value.
    252
    253                 String pattern2 = "org.slf4j.LoggerFactory.getLogger({}.class);";
    254                 String replace = name.replace('/', '.');
    255                 String getLogger = format(pattern2, replace).getMessage();
    256
    257                 cl.addField(field, getLogger);
    258
    259                 // then check every behaviour (which includes methods). We are
    260                 // only
    261                 // interested in non-empty ones, as they have code.
    262                 // NOTE: This will be changed, as empty methods should be
    263                 // instrumented too.
    264
    265                 CtBehavior[] methods = cl.getDeclaredBehaviors();
    266                 for (int i = 0; i < methods.length; i++) {
    267                     if (methods[i].isEmpty() == false) {
    268                         doMethod(methods[i]);
    269                     }
    270                 }
    271                 b = cl.toBytecode();
    272             }
    273         } catch (Exception e) {
    274             System.err.println("Could not instrument " + name + ", " + e);
    275             e.printStackTrace(System.err);
    276         } finally {
    277             if (cl != null) {
    278                 cl.detach();
    279             }
    280         }
    281         return b;
    282     }
    283
    284     /**
    285      * process a single method - this means add entry/exit logging if requested.
    286      * It is only called for methods with a body.
    287      *
    288      * @param method
    289      *          method to work on
    290      * @throws NotFoundException
    291      * @throws CannotCompileException
    292      */
    293     private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
    294
    295         String signature = JavassistHelper.getSignature(method);
    296         String returnValue = JavassistHelper.returnValue(method);
    297
    298         if (addEntryExit) {
    299             String messagePattern = "if ({}.{}()) {}.{}(\">> {}\");";
    300             Object[] arg1 = new Object[] { loggerName, levelEnabled, loggerName, level, signature };
    301             String before = MessageFormatter.arrayFormat(messagePattern, arg1).getMessage();
    302             // System.out.println(before);
    303             method.insertBefore(before);
    304
    305             String messagePattern2 = "if ({}.{}()) {}.{}(\"<< {}{}\");";
    306             Object[] arg2 = new Object[] { loggerName, levelEnabled, loggerName, level, signature, returnValue };
    307             String after = MessageFormatter.arrayFormat(messagePattern2, arg2).getMessage();
    308             // System.out.println(after);
    309             method.insertAfter(after);
    310         }
    311     }
    312 }