/* * $Id$ * * Copyright 2006, The jCoderZ.org Project. 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 of the jCoderZ.org Project 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 REGENTS 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 REGENTS AND 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 org.jcoderz.commons.logging; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.jcoderz.commons.ArgumentMalformedException; import org.jcoderz.commons.types.Date; import org.jcoderz.commons.types.Period; /** * This implements a viewer for existing log files. The output generated by * this should never be viewed using this, since the format might be different * to a 'real' log file. * * TODO: A lot of functionality is still to implement (behaviour according to * command line options). * */ public final class LogViewer implements Runnable { /** Line separator to be used in output files. */ public static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final String DEFAULT_DIR = "." + File.separator + "log"; private static final String DEFAULT_FILE = "log.out"; /** Time interval [ms] used for polling the log file for new data. */ private static final int LOGFILE_POLL_INTERVAL = 100; private static final String [] EMPTY_STRINGS = new String[0]; /** * Level for displaying message stack trace. */ private static final int LEVEL_MESSAGE_STACKTRACE = 2; /** * Level for displaying exception stack trace. */ private static final int LEVEL_EXCEPTION_STACKTRACE = 1; /** * The min length of the date argument. */ private static final int DATE_MIN_LEN = 10; /** Number of tokens expected for a period. */ private static final int PERIOD_TOKEN_COUNT = 2; /** * Command line options. * TODO: Extend options and implement usage. * * The way OptionsBuilder is used is as proposed by the authors * of the commonscli package, but cause checkstyle warnings here... */ private static final Option LOGFILE_OPTION = OptionBuilder.hasArg() .withArgName("logfile").withDescription( "open file <logfile> instead of log.out") .withValueSeparator().withLongOpt("logFile").create("F"); private static final Option LOGDIR_OPTION = OptionBuilder.hasArg() .withArgName("logdir").withDescription( "find log files within <logdir> instead of ./log") .withValueSeparator().withLongOpt("logDir").create("D"); private static final Option LINES_OPTION = OptionBuilder.hasArg() .withArgName("lines").withDescription( "display <lines> last log records instead of 25") .withValueSeparator().withLongOpt("lines").create("l"); private static final Option DATE_OPTION = OptionBuilder.hasOptionalArgs() .withArgName("dateFrom,dateTo").withDescription( "display date [and search for given date/timestamp range. " + "The following date formats are supported: " + "<yyyy-MM-dd|yyyy-MM-dd'T'HH:mm:ss'Z'|" + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'>. One of both dates may be omitted].") .withValueSeparator().withLongOpt("date").create("d"); private static final Option TIME_OPTION = new Option("t", "timestamp", false, "display timestamp."); private static final Option BATCH_OPTION = new Option("b", "batch", false, "batch mode, terminate if end of log file reached."); private static final Option OUTFILE_OPTION = OptionBuilder.hasArg() .withArgName("file").withDescription("send output to file") .withValueSeparator().withLongOpt("outfile").create("o"); private static final Option THREADID_OPTION = OptionBuilder.hasOptionalArgs() .withArgName("tid1,tid2,...").withDescription( "display thread id [and filter for given thread ids") .withValueSeparator().withLongOpt("thread").create("T"); private static final Option XML_OPTION = new Option("xml", "output in xml format."); private static final Option STACKTRACE_OPTION = OptionBuilder.hasArgs() .withArgName("{0|1|2}").withDescription("display stack trace details; " + "0: no stacktrace, 1: only for exceptions (default), " + "2: all stack trace") .withValueSeparator().create("stack"); private static final Option IMPACT_OPTION = OptionBuilder.hasOptionalArgs() .withArgName("bi1,bi2,...").withDescription("display business impact " + "[and filter for given business impact ids]") .withValueSeparator().withLongOpt("impact").create("i"); private static final Option CATEGORY_OPTION = OptionBuilder.hasOptionalArgs() .withArgName("cat1,cat2,...").withDescription("display category " + "[and filter for given categories]") .withValueSeparator().withLongOpt("cat").create("c"); private static final Option LEVEL_OPTION = OptionBuilder.hasOptionalArgs() .withArgName("lev1,lev2,...").withDescription("display log level " + "[and filter for given levels]") .withValueSeparator().withLongOpt("level").create("L"); private static final Option STANDARD_OPTION = OptionBuilder .withDescription("set standard mode, same as -t -i -c -L -stack 1") .withLongOpt("standard").create("s"); /* System.err.println( " -P[id] PID " + "display process ID [and search for given process ID]"); System.err.println( " -r[ecno] [MIN[,MAX]] " + "display record number [and search for given record range]"); System.err.println( " -e[rrordetails] LEVEL " + "set error details to given level"); System.err.println( " -tr[acedetails] LEVEL " + "set trace details to given level"); System.err.println( " -s " + "standard mode: same as -t -r -P -IP"); System.err.println( " -f[ormats] (e|t) " + "display only error or trace messages"); System.err.println( " -lev[el] [<lev1>,[<lev2>]] " + "display message level [and search for given level range]"); System.err.println( " -logger display logger name"); System.err.println(" -class display class name"); System.err.println( " -method display method name"); System.err.println( " -enc[oding] <encoding> " + "define the encoding for standard output,"); System.err.println( " " + "e.g. cp437 (PC US) or cp850 (PC latin1 with Euro)"); System.err.println( " " + "Note: use CHCP to find out the codepage used by DOS."); -log [-l[ines]=NO] - display NO last lines, if NO is -1, ALL lines will be displayed, default: use the last 25 records combine other filter options with -l [-P[id=PID]] [with PID] [-X[id=TxID]] [with TxID] [-S[essionID=SID]] [with SID] [-IP] [with IP] [-p[rogram=NAME]] [with NAME] [-r[ecno]] [show record number] [-e[rrordetails]=LEVEL] [set errordetails to level 'LEVEL'] [-tr[acedetails]=LEVEL] [set tracedetails to level 'LEVEL'] [-s] [standard: same as -t -r -P -IP] [-f[ormats]=(e|t)] [choose only error or trace to be displayed] -lev[el] [<lev1>,[<lev2>]] display message level [and search for given level range] -logger display logger name -class display class name -method display method name -enc[oding] <encoding> define the encoding for standard, */ private PrintWriter mOut; private Options mOptions; private CommandLine mCommandLine; private DisplayOptions mDisplayOptions; private final List mFilters = new ArrayList(); private LogPrinter mDisplay; private LogReader mLogReader; private LogViewer () { installOptions(); } /** * The main entry method. Installs a new instance of this, which reads the * supplied log file. * * @param argv Contains the command line arguments. */ public static void main (String[] argv) { final LogViewer logConsole = new LogViewer(); if (argv.length < 1) { logConsole.printUsage(); } else { try { logConsole.readCommandLineArgs(argv); logConsole.init(); logConsole.run(); } catch (Exception ex) { Throwable th = ex; System.err.println("Caught exception: " + th); while (th != null) { th.printStackTrace(); th = th.getCause(); if (th != null) { System.err.println("Caused by: " + th); } } } finally { logConsole.close(); } } } private void init () throws ArgumentMalformedException, java.text.ParseException { initDisplayOptions(); initDisplayFilters(); setInput(); setOutput(); installDisplay(); } private void initDisplayFilters () throws ArgumentMalformedException, java.text.ParseException { initThreadIdFilter(); initBusinessImpactFilter(); initCategoryFilter(); initLogLevelFilter(); initPeriodFilter(); } private void initThreadIdFilter () { if (mCommandLine.hasOption(THREADID_OPTION.getOpt())) { final String[] threadIds = parseOptionValues( mCommandLine.getOptionValues(THREADID_OPTION.getOpt())); if ((threadIds != null) && (threadIds.length > 0)) { final List ids = new ArrayList(); for (int i = 0; i < threadIds.length; ++i) { ids.add(Long.valueOf(threadIds[i])); } mFilters.add(new ThreadIdFilter(ids)); } } } private void initBusinessImpactFilter () { if (mCommandLine.hasOption(IMPACT_OPTION.getOpt())) { final String[] impacts = parseOptionValues( mCommandLine.getOptionValues(IMPACT_OPTION.getOpt())); if ((impacts != null) && (impacts.length > 0)) { mFilters.add(new BusinessImpactFilter(Arrays.asList(impacts))); } } } private void initCategoryFilter () { if (mCommandLine.hasOption(CATEGORY_OPTION.getOpt())) { final String[] cats = parseOptionValues( mCommandLine.getOptionValues(CATEGORY_OPTION.getOpt())); if ((cats != null) && (cats.length > 0)) { mFilters.add(new CategoryFilter(Arrays.asList(cats))); } } } private void initLogLevelFilter () { if (mCommandLine.hasOption(LEVEL_OPTION.getOpt())) { final String[] levels = parseOptionValues( mCommandLine.getOptionValues(LEVEL_OPTION.getOpt())); if ((levels != null) && (levels.length > 0)) { mFilters.add(new LevelFilter(Arrays.asList(levels))); } } } private void initPeriodFilter () throws ArgumentMalformedException, java.text.ParseException { if (mCommandLine.hasOption(DATE_OPTION.getOpt())) { final Period [] periods = getPeriodsFromOptionValues( mCommandLine.getOptionValues(DATE_OPTION.getOpt())); if ((periods != null) && (periods.length > 0)) { mFilters.add(new PeriodFilter(periods)); } } } static Period [] getPeriodsFromOptionValues (String [] optionValues) throws ArgumentMalformedException, java.text.ParseException { Period [] result = new Period[0]; final List vals = new ArrayList(); for (int i = 0; i < optionValues.length; i++) { final List values = new ArrayList(); final StringTokenizer tokens = new StringTokenizer( optionValues[i], ","); while (tokens.hasMoreTokens()) { values.add(tokens.nextToken()); } String dateFrom = null; String dateTo = null; if (values.size() != PERIOD_TOKEN_COUNT) { if (values.size() == 1 && optionValues[i].startsWith(",", 0)) { dateTo = (String) values.get(0); } else if (values.size() == 1) { dateFrom = (String) values.get(0); } else { throw new LoggingException("Illegal time interval has been " + "specified in the command line " + optionValues[i]); } } else { dateFrom = (String) values.get(0); dateTo = (String) values.get(1); } final Period period = Period.createPeriod(getDateFrom(dateFrom), getDateTo(dateTo)); vals.add(period); } result = (Period []) vals.toArray(result); return result; } static Date getDateFrom (String str) throws java.text.ParseException { return getDateFromOptValue(str, true); } static Date getDateTo (String str) throws java.text.ParseException { return getDateFromOptValue(str, false); } static Date getDateFromOptValue (String str, boolean dateFrom) throws java.text.ParseException { final Date result; if (str == null || str.length() == 0) { result = dateFrom ? Date.OLD_DATE : Date.FUTURE_DATE; } else { if (str.length() == DATE_MIN_LEN) { result = Date.fromString(str + "Z", Date.DATE_FORMAT); } else if (str.length() == DATE_MIN_LEN + 1) { result = Date.fromString(str, Date.DATE_FORMAT); } else { result = Date.fromString(str); } } return result; } /** * Splits the strings contained within the supplied array into single values. * Each String of <code>optionValues</code> might contain a comma separated * value list, which is split by this. * * @param optionValues * * @return String array containing the split values. Might be null. */ private String [] parseOptionValues (final String [] optionValues) { String[] rc = null; if (optionValues != null) { final List values = new ArrayList(); for (int i = 0; i < optionValues.length; ++i) { final StringTokenizer tokens = new StringTokenizer( optionValues[i], "\t ,"); while (tokens.hasMoreTokens()) { values.add(tokens.nextToken()); } } rc = (String[]) values.toArray(EMPTY_STRINGS); } return rc; } /** * */ private void initDisplayOptions () { mDisplayOptions = new DisplayOptions(); if (mCommandLine.hasOption(THREADID_OPTION.getOpt())) { mDisplayOptions.displayThreadId(true); } if (mCommandLine.hasOption(TIME_OPTION.getOpt())) { mDisplayOptions.displayTimestamp(true); } if (mCommandLine.hasOption(DATE_OPTION.getOpt())) { mDisplayOptions.displayTimestamp(true); } if (mCommandLine.hasOption(IMPACT_OPTION.getOpt())) { mDisplayOptions.displayBusinessImpact(true); } if (mCommandLine.hasOption(CATEGORY_OPTION.getOpt())) { mDisplayOptions.displayCategory(true); } if (mCommandLine.hasOption(LEVEL_OPTION.getOpt())) { mDisplayOptions.displayLoggerLevel(true); } if (mCommandLine.hasOption(STACKTRACE_OPTION.getOpt())) { setStackTraceDetails(); } if (mCommandLine.hasOption(STANDARD_OPTION.getOpt())) { setStandardDisplay(mDisplayOptions); } } private void setStandardDisplay (final DisplayOptions dp) { dp.displayTimestamp(true); dp.displayBusinessImpact(true); dp.displayCategory(true); dp.displayLoggerLevel(true); dp.displayStackTrace(true); dp.displayMessageStackTrace(false); } private void setStackTraceDetails () { final String[] details = parseOptionValues( mCommandLine.getOptionValues(STACKTRACE_OPTION.getOpt())); if ((details != null) && (details.length > 0)) { final int level = Integer.parseInt(details[0]); mDisplayOptions.displayMessageStackTrace( level >= LEVEL_MESSAGE_STACKTRACE); mDisplayOptions.displayStackTrace( level >= LEVEL_EXCEPTION_STACKTRACE); } } private void setInput () throws LoggingException { final String logDir = mCommandLine.getOptionValue( LOGDIR_OPTION.getOpt(), DEFAULT_DIR); if ((logDir == null) || (logDir.length() == 0)) { throw new LoggingException("Undefined log directory."); } final String fileName = logDir + File.separator + mCommandLine.getOptionValue( LOGFILE_OPTION.getOpt(), DEFAULT_FILE); final LogReader logReader; try { logReader = new LogReader(fileName); setFilters(logReader); mLogReader = logReader; } catch (InstantiationException ex) { throw new LoggingException("Error instantiating a LogReader for file " + fileName, ex); } } private void setOutput () throws LoggingException { if (mCommandLine.hasOption(OUTFILE_OPTION.getOpt())) { final String fileName = mCommandLine.getOptionValue( OUTFILE_OPTION.getOpt()); try { final File file = new File(fileName); mOut = new PrintWriter(new FileOutputStream(file)); } catch (FileNotFoundException ex) { throw new LoggingException( "Could not open the output file " + fileName, ex); } } else { mOut = new PrintWriter(System.out); } } /** * Closes the log console. Closes the output stream. */ private void close () { if (mOut != null) { mOut.flush(); mOut.close(); } if (mLogReader != null) { mLogReader.close(); } } /** * The run method. * Reads the log file and prints the log records until end of file is reached * (batch mode) or until it is terminated. * * @see java.lang.Runnable#run() */ public void run () { try { if (mCommandLine.hasOption(BATCH_OPTION.getOpt())) { runBatchMode(); } else { runLiveMode(); } } catch (Exception ex) { System.err.println("Caught exception while viewing log file: " + ex); ex.printStackTrace(); } } /** * Runs the live mode. */ private void runLiveMode () { while (true) { LogFileEntry logRecord = null; if (mLogReader.available()) { do { logRecord = mLogReader.readLogFileEntry(); if ((logRecord != null) && (mDisplay != null)) { mDisplay.print(mOut, logRecord); logRecord.release(); } } while (logRecord != null); mOut.flush(); } try { Thread.sleep(LOGFILE_POLL_INTERVAL); } catch (InterruptedException e) { // ignore } } } /** * Runs the batch mode. */ private void runBatchMode () { LogFileEntry logRecord = null; do { logRecord = mLogReader.readLogFileEntry(); if ((logRecord != null) && (mDisplay != null)) { mDisplay.print(mOut, logRecord); logRecord.release(); } } while (logRecord != null); mOut.flush(); } private void installOptions () { mOptions = new Options(); mOptions.addOption(LOGFILE_OPTION); mOptions.addOption(LOGDIR_OPTION); // mOptions.addOption(LINES_OPTION); mOptions.addOption(BATCH_OPTION); mOptions.addOption(OUTFILE_OPTION); mOptions.addOption(XML_OPTION); mOptions.addOption(STACKTRACE_OPTION); mOptions.addOption(DATE_OPTION); mOptions.addOption(TIME_OPTION); mOptions.addOption(THREADID_OPTION); mOptions.addOption(IMPACT_OPTION); mOptions.addOption(CATEGORY_OPTION); mOptions.addOption(LEVEL_OPTION); mOptions.addOption(STANDARD_OPTION); } private void readCommandLineArgs (final String[] args) throws ParseException { mCommandLine = new GnuParser().parse(mOptions, args, true); } private void installDisplay () throws LoggingException { if (mCommandLine.hasOption(XML_OPTION.getOpt())) { try { mDisplay = new XmlPrinter(); } catch (InstantiationException ex) { throw new LoggingException("Error instantiating an XmlPrinter", ex); } } else { mDisplay = new BasicPrinter(); } mDisplay.setDisplayOptions(mDisplayOptions); } /** * Sets the filters for the supplied LogReader as specified in the command * line options. * * @param logReader The LogReader for which to set the filters * * TODO Implement Me */ private void setFilters (final LogReader logReader) { for (final Iterator iter = mFilters.iterator(); iter.hasNext(); ) { logReader.addFilter((Filter) iter.next()); } } private void printUsage () { new HelpFormatter().printHelp( "java org.jcoderz.commons.LogViewer", mOptions); } }