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 }