001/**
002 * Copyright (c) 2004-2011 QOS.ch
003 * All rights reserved.
004 *
005 * Permission is hereby granted, free  of charge, to any person obtaining
006 * a  copy  of this  software  and  associated  documentation files  (the
007 * "Software"), to  deal in  the Software without  restriction, including
008 * without limitation  the rights to  use, copy, modify,  merge, publish,
009 * distribute,  sublicense, and/or sell  copies of  the Software,  and to
010 * permit persons to whom the Software  is furnished to do so, subject to
011 * the following conditions:
012 *
013 * The  above  copyright  notice  and  this permission  notice  shall  be
014 * included in all copies or substantial portions of the Software.
015 *
016 * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
017 * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
018 * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
019 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
020 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
021 * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
022 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
023 *
024 */
025/**
026 *
027 */
028package org.slf4j.instrumentation;
029
030import static org.slf4j.helpers.MessageFormatter.format;
031
032import java.io.ByteArrayInputStream;
033import java.lang.instrument.ClassFileTransformer;
034import java.security.ProtectionDomain;
035
036import javassist.CannotCompileException;
037import javassist.ClassPool;
038import javassist.CtBehavior;
039import javassist.CtClass;
040import javassist.CtField;
041import javassist.NotFoundException;
042
043import org.slf4j.helpers.MessageFormatter;
044
045/**
046 * <p>
047 * LogTransformer does the work of analyzing each class, and if appropriate add
048 * log statements to each method to allow logging entry/exit.
049 * 
050 * <p>
051 * 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"
052 * >Add Logging at Class Load Time with Java Instrumentation</a>.
053 * 
054 */
055public class LogTransformer implements ClassFileTransformer {
056
057    /**
058     * Builder provides a flexible way of configuring some of many options on the
059     * parent class instead of providing many constructors.
060     *
061     * <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>
062     *
063     */
064    public static class Builder {
065
066        /**
067         * Build and return the LogTransformer corresponding to the options set in
068         * this Builder.
069         *
070         * @return
071         */
072        public LogTransformer build() {
073            if (verbose) {
074                System.err.println("Creating LogTransformer");
075            }
076            return new LogTransformer(this);
077        }
078
079        boolean addEntryExit;
080
081        /**
082         * Should each method log entry (with parameters) and exit (with parameters
083         * and return value)?
084         *
085         * @param b
086         *          value of flag
087         * @return
088         */
089        public Builder addEntryExit(boolean b) {
090            addEntryExit = b;
091            return this;
092        }
093
094        boolean addVariableAssignment;
095
096        // private Builder addVariableAssignment(boolean b) {
097        // System.err.println("cannot currently log variable assignments.");
098        // addVariableAssignment = b;
099        // 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}