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 }