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 }