/*
* Copyright (C) 2015
* This program 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.
*
* This program 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/>.
*/
package org.cleverbus.admin.web.msg;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.zip.GZIPInputStream;
import javax.annotation.Nullable;
import org.cleverbus.common.log.Log;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.io.comparator.LastModifiedFileComparator;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Parses log file and returns log lines which corresponds with specified correlation ID.
* <p/>
* Prerequisites/known limitations:
* <ul>
* <li>application logs in DEBUG level
* <li>known log format
* <li>there is only one log file (no increment parts)
* </ul>
*
* @author <a href="mailto:petr.juza@cleverlance.com">Petr Juza</a>
* @author <a href="mailto:tomas.hanus@cleverlance.com">Tomas Hanus</a>
*/
@Component
public class MessageLogParser {
// log file format: logFile_%d{yyyy-MM-dd}_%i.log
private SimpleDateFormat fileFormat = new SimpleDateFormat("yyyy-MM-dd");
public static final String BASE_FILE_EXTENSION = "log";
public static final String GZIP_FILE_EXTENSION = "gz";
@Autowired
private IOFileFilter logNameFilter;
/**
* Absolute path to the folder with log files.
*/
@Value("${log.folder.path}")
private String logFolderPath;
/**
* Gets lines from the log file which corresponds with specified correlation ID.
*
* @param correlationId the correlation ID
* @param logDate which date to search log files for
* @return log lines
* @throws IOException when error occurred during file reading
*/
List<String> getLogLines(String correlationId, Date logDate) throws IOException {
File logFolder = new File(logFolderPath);
if (!logFolder.exists() || !logFolder.canRead()) {
throw new FileNotFoundException("there is no readable log folder - " + logFolderPath);
}
final String logDateFormatted = fileFormat.format(logDate);
// filter log files for current date
IOFileFilter nameFilter = new IOFileFilter() {
@Override
public boolean accept(File file) {
return logNameFilter.accept(file) && (StringUtils.contains(file.getName(), logDateFormatted)
|| file.getName().endsWith(BASE_FILE_EXTENSION));
}
@Override
public boolean accept(File dir, String name) {
return StringUtils.contains(name, logDateFormatted)
|| name.endsWith(BASE_FILE_EXTENSION);
}
};
List<File> logFiles = new ArrayList<File>(FileUtils.listFiles(logFolder, nameFilter, null));
Collections.sort(logFiles, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
// go through all log files
List<String> logLines = new ArrayList<String>();
for (File logFile : logFiles) {
logLines.addAll(getLogLines(logFile, correlationId));
}
return logLines;
}
/**
* Gets lines which corresponds with specified correlation ID from the specified log file.
*
* @param logFile the log file
* @param correlationId the correlation ID
* @return log lines
* @throws IOException when error occurred during file reading
*/
private List<String> getLogLines(File logFile, String correlationId) throws IOException {
List<String> logLines = new ArrayList<String>();
Log.debug("Go through the following log file: " + logFile);
int year = Calendar.getInstance().get(Calendar.YEAR);
String[] possibleYears = new String[] {String.valueOf(year-1), String.valueOf(year)};
InputStream stream = null;
try {
if (logFile.getName().endsWith(GZIP_FILE_EXTENSION)) {
stream = new GZIPInputStream(new BufferedInputStream(new FileInputStream(logFile)));
} else {
stream = new BufferedInputStream(new FileInputStream(logFile));
}
LineIterator it = IOUtils.lineIterator(stream, Charset.defaultCharset());
String requestId = null;
boolean lastCorrectLine = false; // if previous log line belongs to requestId
while (it.hasNext()) {
String line = it.nextLine();
if (requestId == null) {
if (StringUtils.contains(line, correlationId)) {
logLines.add(formatLogLine(line));
// finds requestID
requestId = getRequestId(line);
if (requestId != null) {
Log.debug("correlationId (" + correlationId + ") => requestId (" + requestId + ")");
}
}
} else {
// adds lines with requestID and lines that belongs to previous log record (e.g. XML request)
// it's better to check also correlationID because it's not one request ID
// for all repeated scheduled jobs for processing of partly failed messages
// 2013-05-23 20:22:36,754 [MACHINE_IS_UNDEFINED, ajp-bio-8009-exec-19, /esb/ws/account/v1, ...
// <checkCustomerCreditRequest xmlns="http://cleverbus.org/ws/AccountService-v1">
// <firstName>csd</firstName>
// <lastName>acs</lastName>
// <birthNumber>111111/1111</birthNumber>
// </checkCustomerCreditRequest>
if (StringUtils.contains(line, requestId)
|| (StringUtils.contains(line, correlationId))
|| (lastCorrectLine && !StringUtils.startsWithAny(line, possibleYears))) {
logLines.add(formatLogLine(line));
lastCorrectLine = true;
} else {
lastCorrectLine = false;
}
}
}
} finally {
IOUtils.closeQuietly(stream);
}
return logLines;
}
@Nullable
private String getRequestId(String line) {
// 2013-05-23 20:22:36,754 [MACHINE_IS_UNDEFINED, ajp-bio-8009-exec-19, /esb/ws/account/v1, 10.10.0.95:72cab819:13ecdbd371c:-7eff, ] DEBUG
String logHeader = StringUtils.substringBetween(line, "[", "]");
if (logHeader == null) {
// no match - the line doesn't contain []
return null;
}
String[] headerParts = StringUtils.split(logHeader, ",");
String requestId = StringUtils.trim(headerParts[3]);
// note: if request starts from scheduled job, then there is request ID information
// 2013-05-27 16:37:25,633 [MACHINE_IS_UNDEFINED, DefaultQuartzScheduler-camelContext_Worker-8, , , ]
// WARN c.c.c.i.c.a.d.RepairMessageServiceDbImpl$2 - The message (msg_id = 372, correlationId = ...
return StringUtils.trimToNull(requestId);
}
private String formatLogLine(String line) {
String resLine = StringEscapeUtils.escapeHtml(line);
// highlight ERROR log line
if (StringUtils.contains(line, " ERROR ")
|| StringUtils.contains(line, "StackTrace:")
|| StringUtils.contains(line, "AbstractSoapExceptionFilter - get new exception")) {
resLine = "<span style=\"font-weight: bold; color: red;\">" + resLine + "</span>";
// highlight log lines which contains sent/receive SOAP messages
} else if (StringUtils.contains(line, "MessageTracing.sent")
|| StringUtils.contains(line, "MessageTracing.received")) {
resLine = "<span class=\".soapMessage\">" + resLine + "</span>";
}
return resLine;
}
}