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   * 
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   * 
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 final String level;
139     private final 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 final boolean addEntryExit;
160     // private boolean addVariableAssignment;
161     private final boolean verbose;
162     private final 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 (String s : ignore) {
191                 if (className.startsWith(s)) {
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 (CtBehavior method : methods) {
267                     if (method.isEmpty() == false) {
268                         doMethod(method);
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 }