blob: aa2db5692930300042d4f155bd95e3f760c4b252 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.internal.logging;
import static org.apache.commons.lang3.SystemUtils.LINE_SEPARATOR;
import static org.apache.geode.internal.logging.LogWriterLevel.ALL;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.StringTokenizer;
import org.apache.geode.LogWriter;
import org.apache.geode.internal.ExitCode;
/**
* Parses a log file written by a {@link LogWriter} into {@link LogFileParser.LogEntry}s. It
* behaves sort of like an {@link StringTokenizer}.
*
* @since GemFire 3.0
*/
public class LogFileParser {
private static final boolean TRIM_TIMESTAMPS = Boolean.getBoolean("mergelogs.TRIM_TIMESTAMPS");
private static final boolean NEWLINE_AFTER_HEADER =
Boolean.getBoolean("mergelogs.NEWLINE_AFTER_HEADER");
private static final boolean TRIM_NAMES = Boolean.getBoolean("mergelogs.TRIM_NAMES");
/** Text that signifies the start of a JRockit-style thread dump */
private static final String FULL_THREAD_DUMP = "===== FULL THREAD DUMP ===============";
/** The name of the log file being parsed */
private final String logFileName;
/** The name of the log file plus a colon and space */
private final String extLogFileName;
/** The buffer to read the log file from */
private final BufferedReader br;
/** Are there more entries to parser? */
private boolean hasMoreEntries;
/** The timestamp of the entry being parsed */
private String timestamp;
/** StringBuffer containing the text of the entry we're parsing */
private StringBuffer sb;
/** whether we're still reading the first line of the first entry */
private boolean firstEntry = true;
/**
* StringBuffer containing white space that is the same length as logFileName plus ": ", in a
* monospace font when tabs are 8 chars long
*/
private final StringBuffer whiteFileName;
/** whether to suppress blank lines in output */
private final boolean suppressBlanks;
/**
* Creates a new <code>LogFileParser</code> that reads a log from a given
* <code>BufferedReader</code>. Blanks are not suppressed, and non-timestamped lines are emitted
* as-is.
*
* @param logFileName The name of the log file being parsed. This is appended to the entry. If
* <code>logFileName</code> is <code>null</code> nothing will be appended.
* @param br Where to read the log from
*/
public LogFileParser(final String logFileName, final BufferedReader br) {
this(logFileName, br, false, false);
}
/**
* Creates a new <code>LogFileParser</code> that reads a log from a given
* <code>BufferedReader</code>.
*
* @param logFileName The name of the log file being parsed. This is appended to the entry. If
* <code>logFileName</code> is <code>null</code> nothing will be appended.
* @param br Where to read the log from
* @param tabOut Whether to add white-space to non-timestamped lines to align them with lines
* containing file names.
* @param suppressBlanks whether to suppress blank lines
*/
public LogFileParser(final String logFileName, final BufferedReader br, final boolean tabOut,
final boolean suppressBlanks) {
this.logFileName = logFileName;
this.br = br;
hasMoreEntries = true;
timestamp = null;
sb = new StringBuffer();
this.suppressBlanks = suppressBlanks;
whiteFileName = new StringBuffer();
if (tabOut) {
int numTabs = (logFileName.length() + 2) / 8;
for (int i = 0; i < numTabs; i++) {
whiteFileName.append('\t');
}
for (int i = (logFileName.length() + 2) % 8; i > 0; i--) {
whiteFileName.append(' ');
}
}
if (this.logFileName != null) {
extLogFileName = this.logFileName + ": ";
} else {
extLogFileName = null;
}
}
/**
* Returns whether or not there are any more entries in the file to be parser.
*/
public boolean hasMoreEntries() {
return hasMoreEntries;
}
/**
* copy the timestamp out of a log entry, if there is one, and return it. if there isn't a
* timestamp, return null
*/
private String getTimestamp(final String line) {
int llen = line.length();
String result = null;
if (llen > 10) {
// first see if the start of the line is a timestamp, as in a thread-dump's stamp
if (line.charAt(0) == '2' && line.charAt(1) == '0' && line.charAt(4) == '-'
&& line.charAt(7) == '-') {
return line.substring(0, 19).replace('-', '/');
}
// now look for gemfire's log format
if (line.charAt(0) == '[') {
if (line.charAt(1) == 'i' && line.charAt(2) == 'n'
&& line.charAt(3) == 'f' ||
line.charAt(1) == 'f' && line.charAt(2) == 'i'
&& line.charAt(3) == 'n'
||
line.charAt(1) == 'w' && line.charAt(2) == 'a'
&& line.charAt(3) == 'r'
||
line.charAt(1) == 'd' && line.charAt(2) == 'e'
&& line.charAt(3) == 'b'
||
line.charAt(1) == 't' && line.charAt(2) == 'r'
&& line.charAt(3) == 'a'
||
line.charAt(1) == 's' && line.charAt(2) == 'e'
&& line.charAt(3) == 'v'
||
line.charAt(1) == 'c' && line.charAt(2) == 'o'
&& line.charAt(3) == 'n'
||
line.charAt(1) == 'e' && line.charAt(2) == 'r'
&& line.charAt(3) == 'r'
||
line.charAt(1) == 's' && line.charAt(2) == 'e' && line.charAt(3) == 'c'
&& line.charAt(4) == 'u' && line.charAt(5) == 'r') {
int sidx = 4;
while (sidx < llen && line.charAt(sidx) != ' ') {
sidx++;
}
int endIdx = sidx + 24;
if (endIdx < llen) {
result = line.substring(sidx + 1, endIdx + 1);
}
}
}
}
return result;
}
/**
* Returns the next entry in the log file. The last entry will be an instance of
* {@link LogFileParser.LastLogEntry}.
*/
public LogEntry getNextEntry() throws IOException {
LogEntry entry = null;
while (br.ready()) {
String lineStr = br.readLine();
if (lineStr == null) {
break;
}
int llen = lineStr.length();
int lend = llen;
if (suppressBlanks || firstEntry) {
// trim the end of the line
while (lend > 1 && Character.isWhitespace(lineStr.charAt(lend - 1))) {
lend--;
}
if (lend == 0) {
continue;
}
}
StringBuffer line = new StringBuffer(lineStr);
if (lend != llen) {
line.setLength(lend);
llen = lend;
}
// Matcher matcher = pattern.matcher(line);
String nextTimestamp = getTimestamp(lineStr);
// See if we've found the beginning of a new log entry. If so, bundle
// up the current string buffer and return it in a LogEntry representing
// the currently parsed text
if (nextTimestamp != null) {
if (timestamp != null && TRIM_TIMESTAMPS) {
int tsl = timestamp.length();
if (tsl > 0) {
// find where the year/mo/dy starts and delete it and the time zone
int start = 5;
if (line.charAt(start) != ' ') // info & fine
if (line.charAt(++start) != ' ') // finer & error
if (line.charAt(++start) != ' ') // finest, severe, config
if (line.charAt(++start) != ' ') // warning
start = 0;
if (start > 0) {
line.delete(start + 25, start + 29); // time zone
line.delete(start, start + 11); // date
if (TRIM_NAMES) {
int idx2 = line.indexOf("<", +12);
if (idx2 > start + 13) {
line.delete(start + 13, idx2 - 1);
}
}
}
}
if (NEWLINE_AFTER_HEADER) {
int idx = line.indexOf("tid=");
if (idx > 0) {
idx = line.indexOf("]", idx + 4);
if (idx + 1 < line.length()) {
line.insert(idx + 1, LINE_SEPARATOR + " ");
}
}
}
}
if (timestamp != null) {
entry = new LogEntry(timestamp, sb.toString(), suppressBlanks);
}
timestamp = nextTimestamp;
if (!firstEntry) {
sb = new StringBuffer(500);
} else {
firstEntry = false;
}
if (extLogFileName != null) {
sb.append(extLogFileName);
}
} else if (line.indexOf(FULL_THREAD_DUMP) != -1) {
// JRockit-style thread dumps have time stamps!
String dump = lineStr;
lineStr = br.readLine();
if (lineStr == null) {
break;
}
DateFormat df = DateFormatter.createDateFormat("E MMM d HH:mm:ss yyyy");
df.setLenient(true);
try {
Date date = df.parse(lineStr);
if (timestamp != null) {
// We've found the end of a log entry
entry = new LogEntry(timestamp, sb.toString());
}
df = DateFormatter.createDateFormat();
timestamp = df.format(date);
lineStr = dump;
sb = new StringBuffer();
if (extLogFileName != null) {
sb.append(extLogFileName);
}
sb.append("[dump ");
sb.append(timestamp);
sb.append("]").append(LINE_SEPARATOR).append(LINE_SEPARATOR);
} catch (ParseException ex) {
// Oh well...
sb.append(dump);
}
} else {
sb.append(whiteFileName);
}
sb.append(line);
sb.append(LINE_SEPARATOR);
if (entry != null) {
return entry;
}
}
if (timestamp == null) {
// The file didn't contain any log entries. Just use the
// current time
DateFormat df = DateFormatter.createDateFormat();
// Date now = new Date();
timestamp = df.format(new Date());
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
LocalLogWriter tempLogger = new LocalLogWriter(ALL.intLevel(), pw);
tempLogger.info("MISSING TIME STAMP");
pw.flush();
sb.insert(0, LINE_SEPARATOR + LINE_SEPARATOR);
sb.insert(0, sw.toString().trim());
sb.insert(0, extLogFileName);
}
// Place the final log entry
entry = new LastLogEntry(timestamp, sb.toString());
sb = null;
hasMoreEntries = false;
return entry;
}
/**
* Main program that simply parses a log file and prints out the entries. It is used for testing
* purposes.
*/
public static void main(final String[] args) throws Exception {
if (args.length < 1) {
System.err.println("** Missing log file name");
ExitCode.FATAL.doSystemExit();
}
String logFileName = args[0];
BufferedReader br = new BufferedReader(new FileReader(logFileName));
LogFileParser parser = new LogFileParser(logFileName, br, false, false);
PrintWriter pw = new PrintWriter(System.out);
while (parser.hasMoreEntries()) {
LogEntry entry = parser.getNextEntry();
entry.writeTo(pw);
}
}
/**
* A parsed entry in a log file. Note that we maintain the entry's timestamp as a
* <code>String</code>. {@link DateFormat#parse(String) Parsing} it was too
* expensive.
*/
static class LogEntry {
/** Timestamp of the log entry */
private final String timestamp;
/** The contents of the log entry */
private final String contents;
/** whether extraneous blank lines are being suppressed */
private boolean suppressBlanks;
/**
* Creates a new log entry with the given timestamp and contents
*/
public LogEntry(final String timestamp, final String contents) {
this.timestamp = timestamp;
this.contents = contents;
}
/**
* Creates a new log entry with the given timestamp and contents
*/
public LogEntry(final String timestamp, final String contents, final boolean suppressBlanks) {
this.timestamp = timestamp;
this.contents = contents.trim();
this.suppressBlanks = suppressBlanks;
}
/**
* Returns the timestamp of this log entry
*/
public String getTimestamp() {
return timestamp;
}
/**
* Returns the contents of this log entry
*
* @see #writeTo
*/
String getContents() {
return contents;
}
/**
* Writes the contents of this log entry to a <code>PrintWriter</code>.
*/
public void writeTo(final PrintWriter pw) {
pw.println(contents);
if (!suppressBlanks) {
pw.println("");
}
pw.flush();
}
/**
* Is this entry the last log entry?
*/
public boolean isLast() {
return false;
}
}
/**
* The last log entry read from a log file. We use a separate class to avoid the overhead of an
* extra <code>boolean</code> field in each {@link LogFileParser.LogEntry}.
*/
static class LastLogEntry extends LogEntry {
public LastLogEntry(final String timestamp, final String contents) {
super(timestamp, contents);
}
@Override
public boolean isLast() {
return true;
}
}
}