/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Created on Jun 13, 2006 * * This code belongs to SQL Power Group Inc. */ package ca.sqlpower.util; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import org.apache.log4j.Logger; /** * Implements a "call home, we're broken" functionality - does not report * anything secret. The data are posted to a URL on our web site to keep track * of errors that people see when running the product. * <p> * Additional information that we may want to gather later would be: * <ul> * <li>security manager info * <li>permission to get project file * <li>undo history (need permission) * <li>JDBC drivers and other crap on classpath (need permission) * </ul> */ public class ExceptionReport { private static final Logger logger = Logger.getLogger(ExceptionReport.class); /** * This is the maximum number of tries to send the report before * the send method gives up. */ private static final int MAX_REPORT_TRIES = 10; /** * The number of runs that have occurred so far. If this reaches * the maximum number of reports to try and send the method will * stop trying to send reports. */ private static int numReportsThisRun = 0; /** * Causes this class to remember the current time as application startup time. * You should call this when your app starts if you plan to use this excption * reporting facility. */ public static void init() { logger.debug("Remembering app startup time = " + applicationStartupTime); } /** * The version of the application that this report came from. * The format of the string is not important. */ private String applicationVersion; /** * The time since the application launched in milliseconds. */ private static final long applicationStartupTime = System.currentTimeMillis(); /** * The name of the application that this report came from. */ private String applicationName; /** * The total memory of the system that is running this application. * The value is in bytes and is initialized by the constructor. */ private long totalMem; /** * The amount of free memory of the system at the time this report * is created. The value is in bytes and is initialized by the * constructor. */ private long freeMem; /** * The total memory allocated to the JVM that is running this application. * The value is in bytes and is initialized by the constructor. */ private long maxMem; /** * The vendor of the JVM that is running this application. * The value is initialized by the constructor. */ private String jvmVendor; /** * The version of the JVM that is running this application. * The value is initialized by the constructor. */ private String jvmVersion; /** * The operating system architecture that is running this application. * The value is initialized by the constructor. */ private String osArch; /** * The name of the operating system that is running this application. * The value is initialized by the constructor. */ private String osName; /** * The version of the operating system that is running this application. * The value is initialized by the constructor. */ private String osVersion; /** * Additional information supplied by the application that is given to the * report. This will be passed along when the report is sent. */ private String remarks; /** * The exception that we are creating a report on. */ private Throwable exception; /** * The default URL to send the report to if the URL in the system * properties does not exist. */ private String reportUrl; /** * This map stores additional information that needs to be sent with * the report. The map stores the information in (name, data) pairs. */ private Map<String, String> additionalInfo = new HashMap<String, String>(); /** * This constructor sets all of the necessary properties to send a basic * exception report. * * @param exception * The exception that is being reported on. * @param reportURL * The URL to send the report to. This field cannot be null. * @param applicationVersion * The version of the application that is generating this report. * The format of this string is not important * @param applicationUptime * The time in milliseconds that the application has been running * for. * @param appName * The name of the application that is creating this report. */ public ExceptionReport(Throwable exception, String reportURL, String applicationVersion, String appName) { this.exception = exception; this.reportUrl = reportURL; this.applicationVersion = applicationVersion; this.applicationName = appName; totalMem = Runtime.getRuntime().totalMemory(); freeMem = Runtime.getRuntime().freeMemory(); maxMem = Runtime.getRuntime().maxMemory(); jvmVendor = System.getProperty("java.vendor"); jvmVersion = System.getProperty("java.version"); osArch = System.getProperty("os.arch"); osName = System.getProperty("os.name"); osVersion = System.getProperty("os.version"); } /** * Returns a string that contains the exceptions, system parameters, * and additional information in XML format. */ public String toXML() { StringBuffer xml = new StringBuffer(); xml.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>"); xml.append("\n<application-exception-report version=\"1.0\">"); appendNestedExceptions(xml,exception); xml.append("\n <application-name>").append(SQLPowerUtils.escapeXML(applicationName)).append("</application-name>"); xml.append("\n <application-version>").append(SQLPowerUtils.escapeXML(applicationVersion)).append("</application-version>"); xml.append("\n <application-uptime>").append(getApplicationUptime()).append("</application-uptime>"); xml.append("\n <total-mem>").append(totalMem).append("</total-mem>"); xml.append("\n <free-mem>").append(freeMem).append("</free-mem>"); xml.append("\n <max-mem>").append(maxMem).append("</max-mem>"); xml.append("\n <jvm vendor=\"").append(SQLPowerUtils.escapeXML(jvmVendor)).append("\" version=\"").append(SQLPowerUtils.escapeXML(jvmVersion)).append("\" />"); xml.append("\n <os arch=\"").append(SQLPowerUtils.escapeXML(osArch)).append("\" name=\"").append(SQLPowerUtils.escapeXML(osName)).append("\" version=\"").append(SQLPowerUtils.escapeXML(osVersion)).append("\" />"); for (Map.Entry<String, String> ent : additionalInfo.entrySet()) { xml.append("\n <application-specific" + " property=\"" + SQLPowerUtils.escapeXML(ent.getKey()) + "\">") .append(SQLPowerUtils.escapeXML(ent.getValue())) .append("</application-specific>"); } xml.append("\n <remarks>").append(SQLPowerUtils.escapeXML(remarks)).append("</remarks>"); xml.append("\n</application-exception-report>"); xml.append("\n"); return xml.toString(); } @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append("Exception Report: "); sb.append(exception); sb.append(" "); sb.append(remarks); return sb.toString(); } /** * This appends the exception stack trace to the given string * buffer. * * @param xml * The string buffer to append the exceptions to. * @param exception * The exception that was thrown that will have its * stack added to the buffer. */ private void appendNestedExceptions(StringBuffer xml, Throwable exception) { if (exception == null) return; xml.append("\n <exception class=\"").append(SQLPowerUtils.escapeXML(exception.getClass().getName())).append("\" message=\"") .append(SQLPowerUtils.escapeXML(exception.getMessage())).append("\">"); for (StackTraceElement ste : exception.getStackTrace()) { xml.append("\n <trace-element class=\"").append(SQLPowerUtils.escapeXML(ste.getClassName())) .append("\" method=\"").append(SQLPowerUtils.escapeXML(ste.getMethodName())) .append("\" file=\"").append(SQLPowerUtils.escapeXML(ste.getFileName())) .append("\" line=\"").append(ste.getLineNumber()) .append("\" />"); } appendNestedExceptions(xml,exception.getCause()); xml.append("\n </exception>"); } /** * Sends this report, formatted as an XML document, to the URL given in the * constructor. The report is sent using the HTTP POST method. */ public void send() { logger.debug("posting report: "+toString()); if (numReportsThisRun++ > MAX_REPORT_TRIES) { logger.info( String.format( "Not logging this error, threshold of %d exceeded", MAX_REPORT_TRIES)); return; } exception.printStackTrace(); logger.info("Posting error report to SQL Power at URL <"+reportUrl+">"); try { HttpURLConnection dest = (HttpURLConnection) new URL(reportUrl).openConnection(); dest.setConnectTimeout(3000); dest.setReadTimeout(3000); dest.setDoOutput(true); dest.setDoInput(true); dest.setUseCaches(false); dest.setRequestMethod("POST"); dest.setRequestProperty("Content-Type", "text/xml"); dest.connect(); OutputStream out = null; try { out = new BufferedOutputStream(dest.getOutputStream()); out.write(toXML().getBytes("ISO-8859-1")); out.flush(); } finally { if (out != null) out.close(); } // Note: the error report will only get sent if we attempt to read from the URL Connection (!??!?) InputStreamReader inputStreamReader = new InputStreamReader(dest.getInputStream()); BufferedReader in = new BufferedReader(inputStreamReader); StringBuffer response = new StringBuffer(); String line; while ((line = in.readLine()) != null) { response.append(line); } in.close(); logger.info("Error report servlet response: "+response); } catch (Exception e) { // Just catch-and-squash everything because we're already in up to our necks at this point. logger.error("Couldn't send exception report to <\""+reportUrl+"\">", e); } logger.debug("Finished posting report"); } /** * Inserts a named value and its value to the additional information * mapping. * * @param name * The name of the information that will be put into the report. * @param value * The value of the information that will be put into the report. */ public void addAdditionalInfo(String name, String value) { additionalInfo.put(name, value); } public void setRemarks(String remarks) { this.remarks = remarks; } /** * Returns the number of milliseconds since the static init() method was called. */ public static long getApplicationUptime() { return System.currentTimeMillis() - applicationStartupTime; } }