ThreadDumpCollector.java 17.1 KB
/*
 * Decompiled with CFR 0_118.
 * 
 * Could not load the following classes:
 *  org.apache.felix.inventory.Format
 *  org.apache.felix.inventory.InventoryPrinter
 *  org.apache.felix.scr.annotations.Activate
 *  org.apache.felix.scr.annotations.Component
 *  org.apache.felix.scr.annotations.ConfigurationPolicy
 *  org.apache.felix.scr.annotations.Deactivate
 *  org.apache.felix.scr.annotations.Properties
 *  org.apache.felix.scr.annotations.Property
 *  org.apache.felix.scr.annotations.PropertyOption
 *  org.apache.felix.scr.annotations.Reference
 *  org.apache.sling.commons.osgi.OsgiUtil
 *  org.apache.sling.commons.scheduler.ScheduleOptions
 *  org.apache.sling.commons.scheduler.Scheduler
 *  org.osgi.framework.BundleContext
 *  org.osgi.framework.InvalidSyntaxException
 *  org.osgi.framework.ServiceReference
 *  org.osgi.service.component.ComponentContext
 *  org.slf4j.Logger
 *  org.slf4j.LoggerFactory
 */
package com.adobe.granite.threaddump.impl;

import com.adobe.granite.threaddump.impl.BaseThreadDumpManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Dictionary;
import java.util.Iterator;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipOutputStream;
import org.apache.felix.inventory.Format;
import org.apache.felix.inventory.InventoryPrinter;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyOption;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(metatype=1, name="com.adobe.granite.threaddump.ThreadDumpCollector", label="Granite Thread Dumps Collector", description="Collects and persists Java thread dumps inside compressed GZipped files", policy=ConfigurationPolicy.OPTIONAL)
@Properties(value={@Property(name="scheduler.period", longValue={60}, label="Schedule", description="Interval (in number of seconds) between each thread dump - collector will not be executed if this property value is missing"), @Property(name="scheduler.concurrent", boolValue={0}, label="Concurrent", description="Concurrency is not supported at this version, modifying this parameter has no effect in this version!"), @Property(name="scheduler.runOn", options={@PropertyOption(value="Each node", name="SINGLE"), @PropertyOption(value="Leader only", name="LEADER")}, label="Cluster", description="Set 'Each node' to execute this service on multiple nodes within a cluster, 'Leader only' otherwise")})
public final class ThreadDumpCollector
extends BaseThreadDumpManager
implements Runnable {
    private static final String USER_NAME_SYSTEM_PROPERTY = "user.name";
    private static final String END_OF_DUMP = "\n<EndOfDump>\n\n";
    private static final int DEFAULT_DUMPS_PER_FILE_VALUE = 10;
    private static final int DEFAULT_MAX_BACKUP_DAYS = 7;
    private static final boolean DEFAULT_ENABLE_GZIP_COMPRESSION_VALUE = true;
    private static final boolean DEFAULT_ENABLE_DIRECTORIES_COMPRESSION_VALUE = true;
    private static final boolean DEFAULT_ENABLE_JSTACK = false;
    @Property(label="Enable/Disable", description="Enable or disable the Thread Dumps collection", boolValue={1})
    public static String ENABLED = "granite.threaddump.enabled";
    @Property(label="Dumps per file", description="Number of dumps to be stored in each file", intValue={10})
    public static String DUMPS_PER_FILE = "granite.threaddump.dumpsPerFile";
    @Property(label="GZIP Compression", description="Flag to enable/disable GZIP compression on dump files", boolValue={1})
    public static String ENABLE_GZIP_COMPRESSION = "granite.threaddump.enableGzipCompression";
    @Property(label="Directories Compression", description="Flag to enable/disable ZIP compression on daily dump directories", boolValue={1})
    public static String ENABLE_DIRECTORIES_COMPRESSION = "granite.threaddump.enableDirectoriesCompression";
    @Property(label="Enable JStack", description="Use native JStack JDK application to perform the thread dump", boolValue={0})
    public static String ENABLE_JSTACK = "granite.threaddump.enableJStack";
    @Property(label="Max Backup Days", description="The maximum number of backup files/directories to keep around", intValue={7})
    public static String MAX_BACKUP_DAYS = "granite.threaddump.maxBackupDays";
    private final MessageFormat threadDumpFilterFormat = new MessageFormat("(felix.inventory.printer.name={0,choice,0#|1#jstack-}threaddump)");
    private final MessageFormat dumpFileFormat = new MessageFormat("{0,date,yyyyMMdd}{1}threaddump.txt{2,choice,0#|1#.gz}");
    private final MessageFormat rolledFileFormat = new MessageFormat("{0}{1}{2}.{3,date,HHmmss}.txt{4,choice,0#|1#.gz}");
    private final MessageFormat compressedDirectoryFormat = new MessageFormat("{0,date,yyyyMMdd}{1,choice,0#|1#.zip}");
    private final Logger logger;
    @Reference
    private Scheduler scheduler;
    private int dumpsPerFile;
    private boolean gzipCompressionEnabled;
    private boolean directoriesCompressionEnabled;
    private int currentDumpsPerFile;
    private int maxBackupDays;
    private InventoryPrinter currentThreadDumper;
    private Calendar lastCheck;
    private GZIPOutputStream currentGzipCompressor;
    private PrintWriter currentWriter;
    private boolean deactivated;

    public ThreadDumpCollector() {
        this.logger = LoggerFactory.getLogger(this.getClass());
        this.currentDumpsPerFile = 0;
        this.deactivated = false;
    }

    @Activate
    protected void activate(ComponentContext context) {
        super.activate(context);
        Dictionary contextProperties = context.getProperties();
        boolean enabled = OsgiUtil.toBoolean(contextProperties.get(ENABLED), (boolean)true);
        this.dumpsPerFile = OsgiUtil.toInteger(contextProperties.get(DUMPS_PER_FILE), (int)10);
        this.gzipCompressionEnabled = OsgiUtil.toBoolean(contextProperties.get(ENABLE_GZIP_COMPRESSION), (boolean)true);
        this.directoriesCompressionEnabled = OsgiUtil.toBoolean(contextProperties.get(ENABLE_DIRECTORIES_COMPRESSION), (boolean)true);
        this.maxBackupDays = OsgiUtil.toInteger(contextProperties.get(MAX_BACKUP_DAYS), (int)7);
        if (enabled) {
            boolean enableJstack = OsgiUtil.toBoolean(contextProperties.get(ENABLE_JSTACK), (boolean)false);
            Object[] arrobject = new Object[1];
            arrobject[0] = enableJstack ? 1 : 0;
            String threadDumpFilter = ThreadDumpCollector.format(this.threadDumpFilterFormat, arrobject);
            BundleContext bundleContext = context.getBundleContext();
            try {
                Collection printers = bundleContext.getServiceReferences(InventoryPrinter.class, threadDumpFilter);
                if (printers.isEmpty()) {
                    this.logger.error("Impossible to retrieve org.apache.felix.inventory.InventoryPrinterservice service reference using filter {}, please check the platform to see if services are correctly registered", (Object)threadDumpFilter);
                } else {
                    this.currentThreadDumper = (InventoryPrinter)bundleContext.getService((ServiceReference)printers.iterator().next());
                }
            }
            catch (InvalidSyntaxException e) {
                this.logger.error("Impossible to access to org.apache.felix.inventory.InventoryPrinterservice references", (Throwable)e);
            }
            long period = OsgiUtil.toLong(contextProperties.get("scheduler.period"), (long)-1);
            String runOn = OsgiUtil.toString(contextProperties.get("scheduler.runOn"), (String)"LEADER");
            boolean onLeaderOnly = "LEADER".equals(runOn);
            boolean canRunConcurrently = OsgiUtil.toBoolean(contextProperties.get("scheduler.concurrent"), (boolean)false);
            this.scheduler.schedule((Object)this, this.scheduler.NOW(-1, period).onLeaderOnly(onLeaderOnly).canRunConcurrently(canRunConcurrently).name(this.getClass().getName()));
            this.logger.info("Service successfully configured {storingDirectory: {}, dumpsPerFile: {}, gzipCompressionEnabled: {}}", new Object[]{this.getStoringDirectory(), this.dumpsPerFile, this.gzipCompressionEnabled});
        } else {
            this.logger.info("Service no enabled, it will not dump Threads on the file system");
        }
    }

    @Deactivate
    protected void deactivate() {
        this.deactivated = true;
        this.scheduler.unschedule(this.getClass().getName());
        this.logger.info("Service disabled, Threads will not dumped on the file system.");
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    public void run() {
        if (this.currentThreadDumper == null) {
            this.logger.error("No org.apache.felix.inventory.InventoryPrinterservice service available, impossible to perform the dump");
            return;
        }
        if (this.deactivated) {
            this.closeCurrentOpenStreams();
            this.currentGzipCompressor = null;
            this.currentWriter = null;
            this.deactivated = false;
        }
        Calendar currentDay = Calendar.getInstance();
        String dumpFileName = ThreadDumpCollector.format(this.dumpFileFormat, currentDay.getTime(), File.separator, this.gzipCompressionEnabled());
        File dumpFile = new File(this.getStoringDirectory(), dumpFileName);
        File parent = dumpFile.getParentFile();
        if (!parent.exists() && !parent.mkdirs()) {
            this.logger.warn("Impossible to create the {} directory(ies), please check the current user {} has enough File System privileges, execution is skipped.", new Object[]{parent, System.getProperty("user.name")});
            return;
        }
        try {
            if (!dumpFile.exists() || this.currentWriter == null) {
                this.openCompressedStreams(dumpFile);
            } else if (!ThreadDumpCollector.areTheSameDay(this.lastCheck, currentDay)) {
                this.cleanMaxBackupDays(currentDay);
                this.compressLastCheckedDirectory();
                this.rollWriter(dumpFile);
            }
            this.currentThreadDumper.print(this.currentWriter, Format.TEXT, false);
            this.currentWriter.append("\n<EndOfDump>\n\n");
            this.currentWriter.flush();
            if (this.dumpsPerFile == ++this.currentDumpsPerFile) {
                this.rollWriter(dumpFile);
            }
        }
        catch (IOException e) {
            this.logger.warn("An error occurred while dumping threads, see root causes.", (Throwable)e);
        }
        finally {
            this.lastCheck = currentDay;
        }
    }

    protected void cleanMaxBackupDays(Calendar current) {
        Calendar start = (Calendar)current.clone();
        start.set(5, start.get(5) - this.maxBackupDays);
        this.logger.info("Cleaning backup(s) older than {} days, starting from {}...", (Object)this.maxBackupDays, (Object)start.getTime());
        this.deleteBackup(start);
        this.logger.info("All backup(s) successfully removed.");
    }

    private void deleteBackup(Calendar current) {
        String dirName = ThreadDumpCollector.format(this.compressedDirectoryFormat, current.getTime(), 0);
        File toBeRemoved = new File(this.getStoringDirectory(), dirName);
        if (!toBeRemoved.exists()) {
            String compressedFileName = ThreadDumpCollector.format(this.compressedDirectoryFormat, current.getTime(), 1);
            toBeRemoved = new File(this.getStoringDirectory(), compressedFileName);
        }
        if (toBeRemoved.exists()) {
            this.logger.info("Cleaning old backup stored in {}...", (Object)toBeRemoved);
            ThreadDumpCollector.delete(toBeRemoved);
            current.set(5, current.get(5) - 1);
            this.deleteBackup(current);
        }
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private void compressLastCheckedDirectory() {
        if (!this.directoriesCompressionEnabled) {
            return;
        }
        String sourceFileName = ThreadDumpCollector.format(this.compressedDirectoryFormat, this.lastCheck.getTime(), 0);
        File source = new File(this.getStoringDirectory(), sourceFileName);
        if (!source.exists()) {
            return;
        }
        String targetFileName = ThreadDumpCollector.format(this.compressedDirectoryFormat, this.lastCheck.getTime(), 1);
        File target = new File(this.getStoringDirectory(), targetFileName);
        FileOutputStream fos = null;
        ZipOutputStream zos = null;
        try {
            fos = new FileOutputStream(target);
            zos = new ZipOutputStream(fos);
            ThreadDumpCollector.addToZip(zos, this.getStoringDirectory(), source);
            zos.finish();
        }
        catch (IOException e) {
            this.logger.error("An error occurred while compressing the {} directory to the {} file, see causing errors: {}", new Object[]{source, target, e});
        }
        finally {
            ThreadDumpCollector.closeQuietly(fos);
            ThreadDumpCollector.closeQuietly(zos);
            ThreadDumpCollector.delete(source);
        }
    }

    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    private void rollWriter(File source) throws IOException {
        this.closeCurrentOpenStreams();
        String sourceFileName = source.getName();
        sourceFileName = sourceFileName.substring(0, sourceFileName.indexOf(46));
        String targetFileName = ThreadDumpCollector.format(this.rolledFileFormat, source.getParent(), File.separator, sourceFileName, Calendar.getInstance().getTime(), this.gzipCompressionEnabled());
        File target = new File(targetFileName);
        if (!source.renameTo(target)) {
            this.logger.debug("Impossible to move {} file to {} using renameTo() API, falling back to manual copy.", new Object[]{source, target});
            boolean error = false;
            try {
                ThreadDumpCollector.copyContent(source, new FileOutputStream(target), true);
            }
            catch (IOException e) {
                this.logger.warn("An error occurred while moving {} content to {}, please see the stacktrace: {}", new Object[]{source, target, e});
                error = true;
            }
            finally {
                if (!error) {
                    source.delete();
                }
            }
        }
        this.currentDumpsPerFile = 0;
        this.openCompressedStreams(source);
    }

    private void openCompressedStreams(File target) throws IOException {
        FileOutputStream fos = new FileOutputStream(target, true);
        if (this.gzipCompressionEnabled) {
            this.currentGzipCompressor = new GZIPOutputStream(fos);
            this.currentWriter = new PrintWriter(this.currentGzipCompressor, true);
        } else {
            this.currentGzipCompressor = null;
            this.currentWriter = new PrintWriter(fos, true);
        }
    }

    private void closeCurrentOpenStreams() {
        if (this.currentGzipCompressor != null) {
            try {
                this.currentGzipCompressor.finish();
            }
            catch (IOException e) {
                this.logger.warn("An error occurred while finalizing gzip compression, see thrown exceptions", (Throwable)e);
            }
        }
        ThreadDumpCollector.closeQuietly(this.currentGzipCompressor);
        ThreadDumpCollector.closeQuietly(this.currentWriter);
    }

    private int gzipCompressionEnabled() {
        if (this.gzipCompressionEnabled) {
            return 1;
        }
        return 0;
    }

    private static /* varargs */ String format(MessageFormat format, Object ... arguments) {
        StringBuffer result = new StringBuffer();
        format.format(arguments, result, new FieldPosition(0));
        return result.toString();
    }

    private static boolean areTheSameDay(Calendar lastCheck, Calendar currentDay) {
        if (lastCheck == null) {
            return true;
        }
        return lastCheck.get(1) == currentDay.get(1) && lastCheck.get(6) == currentDay.get(6);
    }

    private static void delete(File file) {
        if (file.isDirectory()) {
            for (File child : file.listFiles()) {
                ThreadDumpCollector.delete(child);
            }
        }
        file.delete();
    }

    protected void bindScheduler(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    protected void unbindScheduler(Scheduler scheduler) {
        if (this.scheduler == scheduler) {
            this.scheduler = null;
        }
    }
}