All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.techempower.gemini.handler.ThreadDumpHandler Maven / Gradle / Ivy

There is a newer version: 3.3.14
Show newest version
/*******************************************************************************
 * Copyright (c) 2018, TechEmpower, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name TechEmpower, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package com.techempower.gemini.handler;

import java.io.*;
import java.lang.management.*;
import java.util.*;

import com.techempower.asynchronous.*;
import com.techempower.gemini.*;
import com.techempower.gemini.configuration.*;
import com.techempower.gemini.path.*;
import com.techempower.gemini.path.annotation.*;
import com.techempower.helper.*;
import com.techempower.text.*;
import com.techempower.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Thread Dump Handler facilitates debugging of production servers through
 * the ability to get a "dump" of the current stack trace for all running
 * Threads. This Handler uses the new "getAllStackTraces" method provided in
 * JDK 1.5's Thread class. This Handler will not function on any platform
 * prior to Java 1.5.
 * 

* ThreadDumpHandler can run in its "classic" mode where * Thread.getAllThreadDumps is used or in an improved mode that uses * java.lang.management ("JMX"). *

* To use ThreadDumpHandler, make sure that the following parameters are set * in your application's .conf file: *

    *
  • ThreadDump.Passphrase - A required configuration parameter that * specifies a passphrase that must be present in the URL parameters to view * the thread dump. This class will throw ConfigurationError during * configuration if this parameter is missing or empty. *
  • ThreadDump.AuthorizedIP - An IP address that is authorized to request a * thread dump. The special option of "any" means requests from any IP address * will be accepted. If additional security is desired, overload the * authorized method in this Handler. *
  • ThreadDump.DumpOnStopLocation - An optional file system location to * write text thread dumps when the application is stopped (such as when it is * reloaded or when the application server shuts down). If empty, no such * files will be written. *
  • ThreadDump.UseJmx - Enabled by default. The JMX mode allows additional * useful information such as lock/monitor details. Disable this mode to use * the "classic" approach of calling Thread.getAllStackTraces. *
*

* Add the ThreadDumpHandler to your application's Dispatcher. *

* To invoke the ThreadDumpHandler, issue a command of this form to your * application in the form of an HTTP request: /threaddump */ public class ThreadDumpHandler extends MethodSegmentHandler implements Configurable, UriAware, Asynchronous { // // Constants. // public static final String DEFAULT_PROPS_PREFIX = "ThreadDump."; public static final int MEGABYTE = 1024 * 1024; public static final String DEFAULT_ROLE = "threaddump"; public static final String ANY_IP = "any"; private static ThreadDumpHandler INSTANCE; // // Member variables. // private String propsPrefix = DEFAULT_PROPS_PREFIX; private String passphrase = null; private String authorizedIP = ANY_IP; private final SynchronizedSimpleDateFormat dateFormatter = new SynchronizedSimpleDateFormat(); private String dumpOnStopLocation = ""; private boolean useJmx = true; private final Logger log = LoggerFactory.getLogger(getClass()); // // Member methods. // /** * Constructor. Sets up references. Subclasses' constructors should * call super(application). * * @param application The application reference * @param propsPrefix Optional properties file attribute name prefix; if * null, the default is "ThreadDump." (including the period.) */ public ThreadDumpHandler(GeminiApplication application, String propsPrefix) { super(application); if (propsPrefix != null) { this.propsPrefix = propsPrefix; } // Make sure that we are going to be configured by the application's // Configurator. application.getConfigurator().addConfigurable(this); INSTANCE = this; } /** * Constructor. * * @param application The application reference */ public ThreadDumpHandler(GeminiApplication application) { this (application, null); } /** * Gets the instance. */ public static ThreadDumpHandler getInstance() { return INSTANCE; } /** * Is the viewer requesting JMX data? */ protected boolean isJmx(Context context) { boolean jmx = this.useJmx; if ( (jmx) && (!query().getBooleanLenient("jmx", jmx)) ) { jmx = false; } return jmx; } /** * Overload this method to provide an authorization check based off of * the Context. The default implementation just checks to see if the * client's IP address matches the IP address specified in the configuration * file (ThreadDump.AuthorizedIP). Also, the passphrase must have been * configured to a non-empty value, and that passphrase must be present as * a request parameter. *

* If the AuthorizedIP is set to "any", a request from any IP address will * be accepted. */ protected boolean isAuthorized(Context context) { return this.passphrase != null && query().get(this.passphrase) != null && (this.authorizedIP.equalsIgnoreCase(ANY_IP) || context.getClientId().equals(this.authorizedIP)); } /** * A simple embedded class used to capture the details of a Thread. */ public static class ThreadDescriptor implements Comparable { private final Thread thread; private final ThreadInfo info; private final StackTraceElement[] stack; public ThreadDescriptor(Thread thread, ThreadInfo info, StackTraceElement[] stack) { this.thread = thread; this.info = info; this.stack = stack; } /** * Gets the thread. */ public Thread getThread() { return this.thread; } /** * Gets the ThreadInfo. */ public ThreadInfo getThreadInfo() { return this.info; } /** * Gets the Stack Trace Elements. */ public StackTraceElement[] getStack() { return this.stack; } @Override public int compareTo(ThreadDescriptor other) { return this.thread.getName().compareTo(other.thread.getName()); } } /** * Gets a sorted Thread map. */ protected static List getThreads(boolean isUsingJmx) { // Call our regular getAllStackTraces. We need to do this even if // we're using JMX since the JMX stuff doesn't get us information like // priority for some reason. final Map currentThreads = Thread.getAllStackTraces(); // If we're using JMX, let's grab JMX information first. ThreadInfo[] allInfos = null; if (isUsingJmx) { allInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true); } final List toReturn = new ArrayList<>(); ThreadDescriptor desc; Thread thread; ThreadInfo info = null; for (Map.Entry entry : currentThreads.entrySet()) { thread = entry.getKey(); // If we're using JMX, find the matching ThreadInfo. if (allInfos != null) { info = null; for (ThreadInfo allInfo : allInfos) { if (allInfo.getThreadId() == thread.getId()) { info = allInfo; break; } } } desc = new ThreadDescriptor(thread, info, entry.getValue()); toReturn.add(desc); } Collections.sort(toReturn); return toReturn; } /** * Executes a thread dump. */ @PathDefault public boolean threadDump(Context context) { final boolean jmx = isJmx(context); final long startTime = System.currentTimeMillis(); log.info("Thread dump requested."); context.setContentType("text/html"); writeHeader(context); final List threadDescs = getThreads(jmx); int half = threadDescs.size() / 2 + threadDescs.size() % 2; int position = 0; context.print("

"); context.print("
"); Iterator threads = threadDescs.iterator(); ThreadDescriptor desc; while (threads.hasNext()) { if (position == half) { context.print("  "); } position++; desc = threads.next(); int priority = desc.thread.getPriority(); context.print("
"); context.print("
"); context.print(desc.thread.getName() + " [ID: " + desc.thread.getId() + "; Priority: " + priority + "]"); context.print("
"); context.print("
"); context.print(desc.thread.toString()); context.print("
"); context.print("
"); StackTraceElement[] stack = desc.stack; if ( (stack != null) && (stack.length > 0) ) { String stackfirstStyle; String stackStyle; if (priority < Thread.NORM_PRIORITY) { stackfirstStyle = "stacklowfirst"; stackStyle = "stacklow"; } else if (priority > Thread.NORM_PRIORITY) { stackfirstStyle = "stackhighfirst"; stackStyle = "stackhigh"; } else { stackfirstStyle = "stackfirst"; stackStyle = "stack"; } for (int i = 0; i < stack.length; i++) { if (i == 0) { context.print("
" + stack[i].toString() + "
"); } else { context.print("
" + stack[i].toString() + "
"); } } } else { context.print("
No stack trace available.
"); } // Do we have additional information from JMX? if (desc.info != null) { LockInfo[] locks = desc.info.getLockedMonitors(); for (int i = 0; i < locks.length; i++) { context.print("
Owns lock on instance " + Integer.toHexString(locks[i].getIdentityHashCode()) + " of " + locks[i].getClassName() + "
"); } long owner = desc.info.getLockOwnerId(); if (desc.info.getLockInfo() != null) { context.print("
Blocked on lock " + Integer.toHexString(desc.info.getLockInfo().getIdentityHashCode()) + " of " + desc.info.getLockInfo().getClassName() + (owner >= 0 ? " owned by thread " + owner + " (" + desc.info.getLockOwnerName() + ")" : "") + "
"); } } context.print("
"); context.print("
"); } context.print("
"); final long msTaken = (System.currentTimeMillis() - startTime); log.info("Thread dump complete, took {} ms.", msTaken); writeFooter(context, msTaken); return true; } /** * Executes a thread dump. */ @PathSegment("plain") public boolean threadDumpPlainText(Context context) { final boolean jmx = isJmx(context); final long startTime = System.currentTimeMillis(); log.info("Thread dump requested."); context.setContentType("text/plain"); context.print("Gemini Thread Dump"); context.print(""); long uptime = app().getUptime(); context.print(this.dateFormatter.format(new Date()) + " - " + DateHelper.getHumanDuration(uptime, 2) + " uptime (" + uptime + " millis) - " + (Runtime.getRuntime().freeMemory() / MEGABYTE) + " MiB free; " + (Runtime.getRuntime().totalMemory() / MEGABYTE) + " MiB allocated - " + app().getVersion().getVerboseDescription()); context.print(""); List threadDescs = getThreads(jmx); Iterator threads = threadDescs.iterator(); ThreadDescriptor desc; while (threads.hasNext()) { desc = threads.next(); int priority = desc.thread.getPriority(); context.print(desc.thread.getName() + " [Priority: " + priority + "]"); context.print(desc.thread.toString()); StackTraceElement[] stack = desc.stack; if ( (stack != null) && (stack.length > 0) ) { for (StackTraceElement aStack : stack) { context.print(" " + aStack.toString()); } } else { context.print("No stack trace available."); } // Do we have additional information from JMX? if (desc.info != null) { LockInfo[] locks = desc.info.getLockedMonitors(); for (LockInfo lock : locks) { context.print(" + Owns lock on instance " + Integer.toHexString( lock.getIdentityHashCode()) + " of " + lock.getClassName()); } long owner = desc.info.getLockOwnerId(); if (desc.info.getLockInfo() != null) { context.print(" - Blocked on lock " + Integer.toHexString(desc.info.getLockInfo().getIdentityHashCode()) + " of " + desc.info.getLockInfo().getClassName() + (owner >= 0 ? " owned by thread " + owner + " (" + desc.info.getLockOwnerName() + ")" : "")); } } context.print(""); } final long msTaken = (System.currentTimeMillis() - startTime); log.info("Thread dump complete, took {} ms.", msTaken); return true; } /** * Writes out an HTML header for the thread dump. */ protected void writeHeader(Context context) { context.print(""); context.print(""); context.print("Gemini Thread Dump"); context.print(""); context.print(""); context.print(""); context.print("

Gemini Thread Dump

"); long uptime = app().getUptime(); context.print("

" + this.dateFormatter.format(new Date()) + " - " + DateHelper.getHumanDuration(uptime, 2) + " uptime (" + uptime + " millis) - " + (Runtime.getRuntime().freeMemory() / MEGABYTE) + " MiB free; " + (Runtime.getRuntime().totalMemory() / MEGABYTE) + " MiB allocated - " + app().getVersion().getVerboseDescription() + "

"); } /** * Writes out an HTML header for the thread dump. */ protected void writeFooter(Context context, long msTaken) { context.print("

Operation took " + msTaken + " ms. Plaintext version

"); context.print(""); context.print(""); } /** * Writes a thread dump file to the specified directory. */ protected void writeDumpFile(String location) { final String filename = location + "thddmp-" + DateHelper.STANDARD_FILENAME_FORMAT.format(new Date()) + ".txt"; log.info("Thread dump: {}", filename); try (PrintWriter pw = new PrintWriter(filename)) { pw.println("Gemini Stop-time Thread Dump"); pw.println(""); long uptime = app().getUptime(); pw.println(this.dateFormatter.format(new Date()) + " - " + DateHelper.getHumanDuration(uptime, 2) + " uptime (" + uptime + " millis) - " + (Runtime.getRuntime().freeMemory() / MEGABYTE) + " MiB free; " + (Runtime.getRuntime().totalMemory() / MEGABYTE) + " MiB allocated - " + app().getVersion().getVerboseDescription()); pw.println(""); List threadDescs = getThreads(this.useJmx); Iterator threads = threadDescs.iterator(); ThreadDescriptor desc; while (threads.hasNext()) { desc = threads.next(); int priority = desc.thread.getPriority(); pw.println(desc.thread.getName() + " [Priority: " + priority + "]"); pw.println(desc.thread.toString()); StackTraceElement[] stack = desc.stack; if ( (stack != null) && (stack.length > 0) ) { for (StackTraceElement aStack : stack) { pw.println(" " + aStack.toString()); } } else { pw.println("No stack trace available."); } // Do we have additional information from JMX? if (desc.info != null) { LockInfo[] locks = desc.info.getLockedMonitors(); for (LockInfo lock : locks) { pw.println(" + Owns lock on instance " + Integer.toHexString( lock.getIdentityHashCode()) + " of " + lock.getClassName()); } long owner = desc.info.getLockOwnerId(); if (desc.info.getLockInfo() != null) { pw.println(" - Blocked on lock " + Integer.toHexString(desc.info.getLockInfo().getIdentityHashCode()) + " of " + desc.info.getLockInfo().getClassName() + (owner >= 0 ? " owned by thread " + owner + " (" + desc.info.getLockOwnerName() + ")" : "")); } } pw.println(""); pw.flush(); } } catch (Exception exc) { log.info("Exception while writing thread dump file: ", exc); } } /** * Configures this component. */ @Override public void configure(EnhancedProperties props) { this.passphrase = props.get(this.propsPrefix + "Passphrase", this.passphrase); this.authorizedIP = props.get(this.propsPrefix + "AuthorizedIP", this.authorizedIP); this.useJmx = props.getBoolean(this.propsPrefix + "UseJmx", this.useJmx); this.dumpOnStopLocation = props.get(this.propsPrefix + "DumpOnStopLocation", this.dumpOnStopLocation); if (StringHelper.isEmptyTrimmed(this.passphrase)) { this.passphrase = null; throw new ConfigurationError("Required configuration parameter '" + this.propsPrefix + "Passphrase' was missing. This must be " + "specified and must have a non-empty value."); } if (StringHelper.isNonEmpty(this.dumpOnStopLocation)) { app().addAsynchronous(this); } else { app().removeAsynchronous(this); } } @Override public void begin() { // Does nothing on begin. } @Override public void end() { // If the dump-on-stop location is specified, write a dump file to the // specified location. if (StringHelper.isNonEmpty(this.dumpOnStopLocation)) { writeDumpFile(this.dumpOnStopLocation); } } } // End ThreadDumpHandler.




© 2015 - 2024 Weber Informatics LLC | Privacy Policy