Home | History | Annotate | Download | only in instrumentation
      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") || level.equals("trace")) {
    129                 this.level = level;
    130             } else {
    131                 if (verbose) {
    132                     System.err.println("level not info/debug/trace : " + level);
    133                 }
    134             }
    135             return this;
    136         }
    137     }
    138 
    139     private String level;
    140     private String levelEnabled;
    141 
    142     private LogTransformer(Builder builder) {
    143         String s = "WARNING: javassist not available on classpath for javaagent, log statements will not be added";
    144         try {
    145             if (Class.forName("javassist.ClassPool") == null) {
    146                 System.err.println(s);
    147             }
    148         } catch (ClassNotFoundException e) {
    149             System.err.println(s);
    150         }
    151 
    152         this.addEntryExit = builder.addEntryExit;
    153         // this.addVariableAssignment = builder.addVariableAssignment;
    154         this.verbose = builder.verbose;
    155         this.ignore = builder.ignore;
    156         this.level = builder.level;
    157         this.levelEnabled = "is" + builder.level.substring(0, 1).toUpperCase() + builder.level.substring(1) + "Enabled";
    158     }
    159 
    160     private boolean addEntryExit;
    161     // private boolean addVariableAssignment;
    162     private boolean verbose;
    163     private String[] ignore;
    164 
    165     public byte[] transform(ClassLoader loader, String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) {
    166 
    167         try {
    168             return transform0(className, clazz, domain, bytes);
    169         } catch (Exception e) {
    170             System.err.println("Could not instrument " + className);
    171             e.printStackTrace();
    172             return bytes;
    173         }
    174     }
    175 
    176     /**
    177      * transform0 sees if the className starts with any of the namespaces to
    178      * ignore, if so it is returned unchanged. Otherwise it is processed by
    179      * doClass(...)
    180      *
    181      * @param className
    182      * @param clazz
    183      * @param domain
    184      * @param bytes
    185      * @return
    186      */
    187 
    188     private byte[] transform0(String className, Class<?> clazz, ProtectionDomain domain, byte[] bytes) {
    189 
    190         try {
    191             for (int i = 0; i < ignore.length; i++) {
    192                 if (className.startsWith(ignore[i])) {
    193                     return bytes;
    194                 }
    195             }
    196             String slf4jName = "org.slf4j.LoggerFactory";
    197             try {
    198                 if (domain != null && domain.getClassLoader() != null) {
    199                     domain.getClassLoader().loadClass(slf4jName);
    200                 } else {
    201                     if (verbose) {
    202                         System.err.println("Skipping " + className + " as it doesn't have a domain or a class loader.");
    203                     }
    204                     return bytes;
    205                 }
    206             } catch (ClassNotFoundException e) {
    207                 if (verbose) {
    208                     System.err.println("Skipping " + className + " as slf4j is not available to it");
    209                 }
    210                 return bytes;
    211             }
    212             if (verbose) {
    213                 System.err.println("Processing " + className);
    214             }
    215             return doClass(className, clazz, bytes);
    216         } catch (Throwable e) {
    217             System.out.println("e = " + e);
    218             return bytes;
    219         }
    220     }
    221 
    222     private String loggerName;
    223 
    224     /**
    225      * doClass() process a single class by first creates a class description from
    226      * the byte codes. If it is a class (i.e. not an interface) the methods
    227      * defined have bodies, and a static final logger object is added with the
    228      * name of this class as an argument, and each method then gets processed with
    229      * doMethod(...) to have logger calls added.
    230      *
    231      * @param name
    232      *          class name (slashes separate, not dots)
    233      * @param clazz
    234      * @param b
    235      * @return
    236      */
    237     private byte[] doClass(String name, Class<?> clazz, byte[] b) {
    238         ClassPool pool = ClassPool.getDefault();
    239         CtClass cl = null;
    240         try {
    241             cl = pool.makeClass(new ByteArrayInputStream(b));
    242             if (cl.isInterface() == false) {
    243 
    244                 loggerName = "_____log";
    245 
    246                 // We have to declare the log variable.
    247 
    248                 String pattern1 = "private static org.slf4j.Logger {};";
    249                 String loggerDefinition = format(pattern1, loggerName).getMessage();
    250                 CtField field = CtField.make(loggerDefinition, cl);
    251 
    252                 // and assign it the appropriate value.
    253 
    254                 String pattern2 = "org.slf4j.LoggerFactory.getLogger({}.class);";
    255                 String replace = name.replace('/', '.');
    256                 String getLogger = format(pattern2, replace).getMessage();
    257 
    258                 cl.addField(field, getLogger);
    259 
    260                 // then check every behaviour (which includes methods). We are
    261                 // only
    262                 // interested in non-empty ones, as they have code.
    263                 // NOTE: This will be changed, as empty methods should be
    264                 // instrumented too.
    265 
    266                 CtBehavior[] methods = cl.getDeclaredBehaviors();
    267                 for (int i = 0; i < methods.length; i++) {
    268                     if (methods[i].isEmpty() == false) {
    269                         doMethod(methods[i]);
    270                     }
    271                 }
    272                 b = cl.toBytecode();
    273             }
    274         } catch (Exception e) {
    275             System.err.println("Could not instrument " + name + ", " + e);
    276             e.printStackTrace(System.err);
    277         } finally {
    278             if (cl != null) {
    279                 cl.detach();
    280             }
    281         }
    282         return b;
    283     }
    284 
    285     /**
    286      * process a single method - this means add entry/exit logging if requested.
    287      * It is only called for methods with a body.
    288      *
    289      * @param method
    290      *          method to work on
    291      * @throws NotFoundException
    292      * @throws CannotCompileException
    293      */
    294     private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
    295 
    296         String signature = JavassistHelper.getSignature(method);
    297         String returnValue = JavassistHelper.returnValue(method);
    298 
    299         if (addEntryExit) {
    300             String messagePattern = "if ({}.{}()) {}.{}(\">> {}\");";
    301             Object[] arg1 = new Object[] { loggerName, levelEnabled, loggerName, level, signature };
    302             String before = MessageFormatter.arrayFormat(messagePattern, arg1).getMessage();
    303             // System.out.println(before);
    304             method.insertBefore(before);
    305 
    306             String messagePattern2 = "if ({}.{}()) {}.{}(\"<< {}{}\");";
    307             Object[] arg2 = new Object[] { loggerName, levelEnabled, loggerName, level, signature, returnValue };
    308             String after = MessageFormatter.arrayFormat(messagePattern2, arg2).getMessage();
    309             // System.out.println(after);
    310             method.insertAfter(after);
    311         }
    312     }
    313 }