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 }