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}