/** * Copyright 2005-2016 Red Hat, Inc. * * Red Hat licenses this file to you under the Apache License, version * 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package io.fabric8.apmagent.strategy.trace; import io.fabric8.apmagent.ApmConfiguration; import io.fabric8.apmagent.ClassInfo; import io.fabric8.apmagent.Strategy; import io.fabric8.apmagent.metrics.ApmAgentContext; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.util.CheckClassAdapter; import org.slf4j.LoggerFactory; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicBoolean; public class TraceStrategy implements Strategy, ClassFileTransformer { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(TraceStrategy.class); private ApmAgentContext context; private ApmConfiguration configuration; private Instrumentation instrumentation; private BlockingQueue<Class<?>> blockingQueue = new LinkedBlockingDeque<>(); private AtomicBoolean initialized = new AtomicBoolean(); private AtomicBoolean started = new AtomicBoolean(); private AtomicBoolean cleanUp = new AtomicBoolean(); private Thread transformThread; public TraceStrategy(ApmAgentContext context, Instrumentation instrumentation) { this.context = context; this.configuration = context.getConfiguration(); this.instrumentation = instrumentation; } @Override public void initialize() throws Exception { if (initialized.compareAndSet(false, true)) { configuration.addChangeListener(this); } } @Override public void start() throws Exception { if (started.compareAndSet(false, true)) { initialize(); instrumentApplication(); } } @Override public void stop() { if (started.compareAndSet(true, false)) { // noop } } @Override public void shutDown() { if (initialized.compareAndSet(true, false)) { stop(); configuration.removeChangeListener(this); instrumentation.removeTransformer(this); Thread t = transformThread; transformThread = null; if (t != null && !t.isInterrupted()) { t.interrupt(); } cleanUp.set(true); try { //clean up instrumentApplication(); } catch (Throwable e) { LOG.warn("Failed to shutdown due " + e.getMessage() + ". This exception is ignored.", e); } } } public boolean isAudit(String className) { return configuration.isAudit(className); } public boolean isAudit(String className, String methodName) { return configuration.isAudit(className, methodName); } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { byte[] buffer = null; ClassInfo classInfo = context.getClassInfo(className); classInfo.setOriginalClass(classBeingRedefined); if (classInfo.getTransformed() == null) { //we haven't been transformed before classInfo.setOriginal(classfileBuffer); } if (!cleanUp.get()) { byte[] classBufferToRedefine = classInfo.getOriginal(); if (configuration.isAudit(className)) { if (classInfo.isTransformed()) { //remove metrics from methods no longer defined context.resetMethods(classInfo); } ClassReader cr = new ClassReader(classBufferToRedefine); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); ApmClassVisitor visitor = new ApmClassVisitor(this, cw, classInfo); cr.accept(visitor, ClassReader.SKIP_FRAMES); buffer = cw.toByteArray(); if (!verifyClass(className, buffer)) { classInfo.setCanTransform(false); buffer = null; } classInfo.setTransformed(buffer); } } else { if (classInfo.getOriginal() != null) { buffer = classInfo.getOriginal(); context.resetAll(classInfo); } } return buffer; } @Override public void configurationChanged() { if (started.get()) { if (configuration.isFilterChanged()) { List<ClassInfo> deltas = context.buildDeltaList(); if (deltas != null && !deltas.isEmpty()) { for (ClassInfo classInfo : deltas) { if (configuration.isAsyncTransformation()) { try { blockingQueue.put(classInfo.getOriginalClass()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { try { instrumentation.retransformClasses(new Class[]{classInfo.getOriginalClass()}); } catch (Throwable e) { LOG.warn("Could not transform " + classInfo.getClassName() + " due " + e.getMessage(), e); } } } if (configuration.isAsyncTransformation() && !blockingQueue.isEmpty()) { startTransformThread(); } } } } } public Instrumentation getInstrumentation() { return instrumentation; } public void setInstrumentation(Instrumentation instrumentation) { this.instrumentation = instrumentation; } public ApmAgentContext getContext() { return context; } public void setContext(ApmAgentContext context) { this.context = context; this.configuration = context.getConfiguration(); } private void instrumentApplication() throws FileNotFoundException, UnmodifiableClassException { if (!instrumentation.isRetransformClassesSupported()) { throw new UnmodifiableClassException(); } instrumentation.addTransformer(this, true); for (Class<?> c : instrumentation.getAllLoadedClasses()) { if (isInstrumentClass(c)) { if (configuration.isAsyncTransformation()) { try { blockingQueue.put(c); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { try { instrumentation.retransformClasses(new Class[]{c}); } catch (Throwable e) { LOG.error("Could not transform " + c.getName(), e); } } } } if (configuration.isAsyncTransformation() && !blockingQueue.isEmpty()) { startTransformThread(); } } private boolean isInstrumentClass(Class c) { if (!instrumentation.isModifiableClass(c)) { LOG.trace("NO INSTRUMENT: Class {} is not modifiable", c.getName()); return false; } if (!configuration.isAudit(c.getName())) { LOG.trace("NO INSTRUMENT: Class {} is blacklisted", c.getName()); return false; } if (c.isArray() || c.isAnnotation() || c.isInterface() || c.isPrimitive() || c.isSynthetic() || c.isEnum()) { LOG.trace("NO INSTRUMENT: Class {} is an array, primitive, annotation or enum etc.", c.getName()); return false; } return true; } private synchronized void startTransformThread() { if (configuration.isAsyncTransformation() && transformThread == null) { transformThread = new Thread(new Runnable() { @Override public void run() { while (initialized.get() && !blockingQueue.isEmpty()) { try { Class<?> aClass = blockingQueue.take(); if (aClass != null) { if (isInstrumentClass(aClass)) { try { instrumentation.retransformClasses(new Class[]{aClass}); } catch (Throwable e) { LOG.error("Could not transform " + aClass.getName(), e); } } } } catch (InterruptedException e) { shutDown(); } } } }); transformThread.setDaemon(true); transformThread.start(); } } private boolean verifyClass(String className, byte[] transformed) { boolean result = true; if (configuration.isVerifyClasses()) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); CheckClassAdapter.verify(new ClassReader(transformed), false, pw); if (sw.toString().length() != 0) { result = false; LOG.error("Failed to transform class: " + className); LOG.error(sw.toString()); } } return result; } }