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") || 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 }