/* * Copyright (c) 2014. Real Time Genomics Limited. * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. 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. * * 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 THE COPYRIGHT * HOLDER OR CONTRIBUTORS 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.rtg.util.diagnostic; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import com.rtg.util.Environment; import com.rtg.util.License; import com.rtg.util.NullStreamUtils; import com.rtg.util.StringUtils; import com.rtg.util.gzip.GzipUtils; import com.rtg.util.io.FileUtils; import com.rtg.util.io.LogFile; import com.rtg.util.io.LogSimple; import com.rtg.util.io.LogStream; import htsjdk.samtools.util.Log; /** * Utility class for controlling the dispatch of diagnostic warnings, errors, * and progress information in SLIM. Anyone implementing <code>DiagnosticListener</code> * can register to receive diagnostic events, and anyone can generate such events * by calling the utility methods in this class. * * <p>A facility is also provided for logging of text messages to a stream. By * default such output is sent to <code>System.err</code>, but any other * <code>PrintStream</code> can be supplied. * */ public final class Diagnostic { /** flush interval used for flushing logs */ private static final int FLUSH_INTERVAL = 1000; private Diagnostic() { } /** Maintains all current listeners. */ private static final HashSet<DiagnosticListener> LISTENERS = new HashSet<>(); /** Current stream for logging, if any. */ private static LogStream sLogStream = new LogSimple(System.err); /** True if the current log is closed. */ private static boolean sLogClosed = false; private static LogStream sProgressStream = null; private static boolean sProgressClosed = false; private static String sLastProgress = ""; /** Tracks if we have reported logging redirection message. */ private static boolean sLogRedirect = false; /** tracks when the last flush happened and attempts to flush every minute **/ private static long sLastFlushTime = 0; /** * Remove all listeners. */ public static void clearListeners() { LISTENERS.clear(); } /** * Add the given listener to the those notified whenever a diagnostic occurs. * Requests to add null are ignored. Adding the same listener multiple times * is the same as adding it once. * * @param listener listener to add */ public static void addListener(final DiagnosticListener listener) { if (listener != null) { LISTENERS.add(listener); } } /** * Remove a listener from those notified whenever a diagnostic occurs. Requests * to remove a listener not currently registered are ignored. * * @param listener listener to remove */ public static void removeListener(final DiagnosticListener listener) { if (listener != null) { listener.close(); } LISTENERS.remove(listener); } /** * Pass the given diagnostic event to all currently registered listeners. * Listeners should not modify the event. Null events are not passed. * * @param event the event */ public static void notifyAll(final DiagnosticEvent<?> event) { if (event != null) { for (final DiagnosticListener listener : LISTENERS) { listener.handleDiagnosticEvent(event); } } } /** * Convenience method to report a warning. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. * * @param type type of warning * @param params parameters of warning */ public static void warning(final WarningType type, final String... params) { if (type != null && type.getNumberOfParameters() == params.length) { final WarningEvent event = new WarningEvent(type, params); userLog(event.getMessage()); notifyAll(event); } } /** * Convenience method to send a message to the user. This raises a warning event * of type <code>WarningType.INFO_WARNING</code>. * @param message message to print */ public static void warning(final String message) { warning(WarningType.INFO_WARNING, message); } /** * Convenience method to report an error to the log only. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. * * @param type type of error * @param params parameters of error */ public static void errorLogOnly(final ErrorType type, final String... params) { if (type != null && type.getNumberOfParameters() == params.length) { final ErrorEvent event = new ErrorEvent(type, params); userLog(event.getMessage()); } } /** * Convenience method to report an error. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. * * @param type type of error * @param params parameters of error */ public static void errorNoLog(final ErrorType type, final String... params) { if (type != null && type.getNumberOfParameters() == params.length) { final ErrorEvent event = new ErrorEvent(type, params); //System.err.println(event.getMessage()); notifyAll(event); } } /** * Convenience method to report an error. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. Any error * reported here, is also sent to the log. * * @param type type of error * @param params parameters of error */ public static void error(final ErrorType type, final String... params) { if (type != null && type.getNumberOfParameters() == params.length) { final ErrorEvent event = new ErrorEvent(type, params); userLog(event.getMessage()); notifyAll(event); } } /** * Convenience method to report an error. Uses <code>INFO_ERROR</code> to send * error event. * @param message the message for the error. */ public static void error(String message) { error(ErrorType.INFO_ERROR, message); } /** * Convenience method to report user information. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. * * @param type type of information * @param params parameters of user information */ public static void info(final InformationType type, final String... params) { info(type, false, params); } /** * Convenience method to report user information. If the <code>type</code> is * <code>null</code> or if the parameter count does not match the expected * number of parameters then the notification is ignored. * * @param type type of information * @param outputToProgress true if you want to output the message to the progress stream * @param params parameters of user information */ public static void info(final InformationType type, boolean outputToProgress, final String... params) { if (type != null && type.getNumberOfParameters() == params.length) { final InformationEvent event = new InformationEvent(type, params); userLog(event.getMessage()); if (outputToProgress) { Diagnostic.progress(event.getMessage()); } notifyAll(event); } } /** * Convenience method to report information to user. Uses <code>INFO_USER</code> to send * information event. * @param message the message for the user. */ public static void info(String message) { info(InformationType.INFO_USER, message); } /** * Set the stream to which logging messages are sent to null. This is permissible, * but unadvisable, as setting it to null causes all * logging information to be discarded. By default to logging stream * is System.err. * However, this is handy for testing. */ public static void setLogStream() { setLogStream((LogStream) null); } /** * Set the stream to which logging messages are sent. It is permissible, * but unadvisable to set this to null, as setting it to null causes all * logging information to be discarded. By default to logging stream * is System.err. * * @param logStream stream to use for logging * @return the log stream created */ public static LogStream setLogStream(final PrintStream logStream) { final LogStream ret = new LogSimple(logStream); setLogStream(ret); return ret; } /** * Set the stream to which logging messages are sent. It is permissible, * but unadvisable to set this to null, as setting it to null causes all * logging information to be discarded. * * @param logStream stream to use for logging */ public static synchronized void setLogStream(final LogStream logStream) { sLogRedirect = false; if (sLogStream == logStream) { return; } if (sLogStream != null && logStream != null && logStream.stream() == sLogStream.stream()) { return; } if (sLogStream != null) { closeLog(); } sLogStream = logStream; Log.setGlobalPrintStream(sLogStream == null ? NullStreamUtils.getNullPrintStream() : sLogStream.stream()); sLogClosed = false; sProgressClosed = false; } /** * @return the stream currently used for log messages. */ static synchronized PrintStream getLogStream() { if ((sLogStream == null) || sLogClosed) { return null; } return sLogStream.stream(); } /** * @return the stream currently used for progress messages. */ static synchronized PrintStream getProgressStream() { if (sProgressClosed || sProgressStream == null && (sLogStream == null || sLogStream.file() == null)) { return null; } if (sProgressStream == null) { final File progressFile = new File(sLogStream.file().getParentFile(), FileUtils.PROGRESS_SUFFIX); sProgressStream = new LogFile(progressFile); } return sProgressStream.stream(); } /** * Switch the log to a (usually) different output file. * @param log the name of the file where the log is to be written. * @return the log (so it can be closed). */ public static synchronized LogStream switchLog(final String log) { final File newLog = new File(log); return switchLog(newLog); } /** * Switch the log to a (usually) different output file. * @param newLog the file where the log is to be written. * @return the log (so it can be closed). */ public static synchronized LogStream switchLog(final File newLog) { if (sLogStream != null) { userLog("Switching logfile to:" + newLog.getAbsolutePath()); final File file = sLogStream.file(); if (newLog.equals(file)) { return sLogStream; } //System.err.println("Will delete: " + file.getPath()); sLogStream.removeLog(); } //System.err.println("Switching logfile to:" + log); if (!newLog.getParentFile().exists()) { if (!newLog.getParentFile().mkdirs()) { throw new RuntimeException("Unable to create directory for log file."); } } sLogStream = new LogFile(newLog); sLogClosed = false; logEnvironment(); return sLogStream; } /** * Delete the file currently used for logging. This should only be called * when you are certain the current logging file is not required. After * calling this, the current logging stream is set to <code>null</code>. */ public static synchronized void deleteLog() { if (sLogStream != null) { sLogStream.removeLog(); setLogStream(); } } /** * Closes the current log stream, should be used at the end of a main to ensure * any logging files are closed if it is difficult to ensure a created log stream * is closed. */ public static synchronized void closeLog() { if (sLogStream != null) { sLogStream.close(); sLogClosed = true; } if (sProgressStream != null) { sProgressStream.close(); sProgressClosed = true; sProgressStream = null; } } /** * Write a message to the log stream. If writing to the log stream fails * and the log stream differs from <code>System.err</code>, then an attempt * is made to redirect the message to <code>System.err</code>. * * @param message to write */ public static synchronized void userLog(final String message) { userLog(message, ""); } /** * Write a message to the log stream. If writing to the log stream fails * and the log stream differs from <code>System.err</code>, then an attempt * is made to redirect the message to <code>System.err</code>. * * @param message to write * @param prefix a prefix to include before the message */ private static synchronized void userLog(final String message, final String prefix) { final PrintStream log = getLogStream(); if (log != null) { log.println(now() + prefix + message); final long currentTimeMillis = System.currentTimeMillis(); if (currentTimeMillis - sLastFlushTime > FLUSH_INTERVAL) { log.flush(); sLastFlushTime = currentTimeMillis; } if (log.checkError()) { // The call to checkError forces a flush and returns true if the stream // is in an unusable state. This should not happen in ordinary usage, // but could happen if for example logging is going to a file and the // disk runs out of space. Therefore, if the current logging stream is // not System.err, we make an effort to redirect the log message to // that stream. if (!log.equals(System.err)) { if (!sLogRedirect) { sLogRedirect = true; System.err.println("Logging problem: redirecting logging to System.err."); setLogStream(System.err); } System.err.println(now() + message); System.err.flush(); } } } } /** * Write a throwable to the log stream. If writing to the log stream fails * and the log stream differs from <code>System.err</code>, then an attempt * is made to redirect the message to <code>System.err</code>. * * @param t throwable to write */ public static synchronized void userLog(final Throwable t) { final PrintStream log = getLogStream(); if (log != null) { t.printStackTrace(log); log.flush(); if (log.checkError()) { // The call to checkError forces a flush and returns true if the stream // is in an unusable state. This should not happen in ordinary usage, // but could happen if for example logging is going to a file and the // disk runs out of space. Therefore, if the current logging stream is // not System.err, we make an effort to redirect the log message to // that stream. if (!log.equals(System.err)) { if (!sLogRedirect) { sLogRedirect = true; System.err.println(now() + "Logging problem: redirecting logging to System.err."); } t.printStackTrace(System.err); System.err.flush(); } } } } private static void logVersion() { userLog("RTG version = " + Environment.getVersion()); } /** * Write a bunch of environmental information to the log stream, in the form * of <code>(key,value)</code> pairs. */ public static synchronized void logEnvironment() { userLog("serial = " + License.getSerialNumber()); userLog("email = " + License.getPersonEmail()); logVersion(); final SortedMap<String, String> env = new TreeMap<>(Environment.getEnvironmentMap()); for (final Map.Entry<String, String> e : env.entrySet()) { userLog(e.getKey() + " = " + e.getValue()); } userLog("gzipfix.enabled = " + GzipUtils.getOverrideGzip()); } /** * Get the current date and time as a string of the form <code>YYYY-MM-DD hh:mm:ss</code>. * * @return date string */ public static String now() { final StringBuilder sb = new StringBuilder(); final Calendar cal = new GregorianCalendar(); sb.append(cal.get(Calendar.YEAR)).append('-'); final int month = 1 + cal.get(Calendar.MONTH); if (month < 10) { sb.append('0'); } sb.append(month).append('-'); final int date = cal.get(Calendar.DATE); if (date < 10) { sb.append('0'); } sb.append(date).append(' '); final int hour = cal.get(Calendar.HOUR_OF_DAY); if (hour < 10) { sb.append('0'); } sb.append(hour).append(':'); final int min = cal.get(Calendar.MINUTE); if (min < 10) { sb.append('0'); } sb.append(min).append(':'); final int sec = cal.get(Calendar.SECOND); if (sec < 10) { sb.append('0'); } sb.append(sec).append(' '); return sb.toString(); } static File getLogFile() { return sLogStream == null ? null : sLogStream.file(); } /** * Write a message to the log stream for developers only. * If writing to the log stream fails and the log stream differs from * <code>System.err</code>, then an attempt is made to redirect the message * to <code>System.err</code>. * * @param message to write */ public static void developerLog(final String message) { if (License.isDeveloper()) { userLog(message, "\t\t "); // prefix developer only log messages so we can visually separate developers versus customers } } /** * Write a throwable to the log stream for developers only. * If writing to the log stream fails and the log stream differs from * <code>System.err</code>, then an attempt is made to redirect the message * to <code>System.err</code>. * * @param t throwable to write */ public static void developerLog(final Throwable t) { if (License.isDeveloper()) { userLog(t); } } /** * Write a message to the progress stream if a log file exists. * @param message to write */ public static synchronized void progress(final String message) { sLastProgress = message; final PrintStream prog = getProgressStream(); if (prog != null) { prog.println(now() + message); prog.flush(); if (prog.checkError()) { // The call to checkError forces a flush and returns true if the stream // is in an unusable state. This should not happen in ordinary usage, // but could happen if for example logging is going to a file and the // disk runs out of space. sProgressStream = null; } } } /** * Gets the last progress message. * * @return the last progress message. */ public static synchronized String lastProgress() { return sLastProgress; } private static final String OOM_ERROR_MESSAGE = StringUtils.LS + new ErrorEvent(ErrorType.NOT_ENOUGH_MEMORY).getMessage() + StringUtils.LS; private static final byte[] OOM_ERROR_MESSAGE_BYTES = OOM_ERROR_MESSAGE.getBytes(); private static final OutputStream OOM_ERROR_STREAM = FileUtils.getStderrAsOutputStream(); /** * Prints a message about being out of memory, without allocating additional memory. */ public static void oomMessage() { try { OOM_ERROR_STREAM.write(OOM_ERROR_MESSAGE_BYTES); OOM_ERROR_STREAM.flush(); } catch (final IOException e) { // can't do anything else here without allocating more memory } } }