    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 * </p>
    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 * </p>
    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 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}