FileLogReader.java

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.common.logging.file.logback;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;

import org.eclipse.rdf4j.common.logging.LogLevel;
import org.eclipse.rdf4j.common.logging.LogRecord;
import org.eclipse.rdf4j.common.logging.base.AbstractLogReader;
import org.eclipse.rdf4j.common.logging.base.SimpleLogRecord;

import ch.qos.logback.core.Appender;
import ch.qos.logback.core.FileAppender;

/**
 * File log reader
 */
public class FileLogReader extends AbstractLogReader {

	private File logFile = null;
	private RandomAccessFile log = null;

	private long fileLength;
	private long byteOffset;

	private LogRecord next = null;
	private int count = 0;

	/**
	 * Constructor
	 */
	public FileLogReader() {
	}

	/**
	 * Constructor
	 *
	 * @param logFile
	 */
	public FileLogReader(File logFile) {
		this.logFile = logFile;
	}

	@Override
	public void setAppender(Appender<?> appender) {
		super.setAppender(appender);
		if (appender instanceof FileAppender) {
			this.logFile = new File(((FileAppender<?>) appender).getFile());
		} else {
			throw new RuntimeException("FileLogReader appender must be an instance of FileAppender!");
		}
		this.next = null;
	}

	@Override
	public void init() throws Exception {
		if (logFile == null) {
			throw new RuntimeException("Log file is undefined for this FileLogReader!");
		}
		if (log != null) {
			log.close();
		}
		log = new RandomAccessFile(logFile, "r");
		fileLength = log.length();
		byteOffset = fileLength - 1;
		count = 0;
		next = getNext();
		if (getOffset() > 0) {
			doSkip(getOffset());
		}
	}

	/**
	 * Skip for a specific offset
	 *
	 * @param offset offset
	 */
	private void doSkip(int offset) {
		while (this.hasNext() && (count < offset)) {
			this.next();
		}
	}

	@Override
	public boolean isMoreAvailable() {
		return next != null;
	}

	@Override
	public boolean hasNext() {
		if (getLimit() == 0) {
			return isMoreAvailable();
		}
		return isMoreAvailable() && (count < (getOffset() + getLimit()));
	}

	@Override
	public LogRecord next() {
		LogRecord result = next;
		try {
			next = getNext();
			count++;
		} catch (IOException ioe) {
			throw new RuntimeException("Unable to get next log record.", ioe);
		}
		if (!hasNext()) {
			try {
				destroy();
			} catch (IOException e) {
				// too bad
			}
		}
		return result;
	}

	/**
	 * Get next log record
	 *
	 * @return log record
	 * @throws IOException
	 */
	private LogRecord getNext() throws IOException {
		SimpleLogRecord result = null;

		StringBuilder message = new StringBuilder();

		List<String> stackTrace = new LinkedList<>();

		while (result == null && byteOffset > 0) {
			List<Byte> bytesRead = new LinkedList<>();
			if (byteOffset < 0) {
				System.err.println("Subzero byteOffset with: ");
				System.err.println("\tMessage: " + message);
				System.err.println("\tStacktrace: " + stackTrace.size());
			}
			// find start of previous line
			byte currentByte;
			do {
				log.seek(byteOffset--);
				currentByte = log.readByte();
				if (currentByte != '\n' && currentByte != '\r') {
					bytesRead.add(0, currentByte);
				}
			} while (byteOffset > 0 && currentByte != '\n' && currentByte != '\r');

			// if at start of file, retrieve the byte we just read in the do/while
			if (byteOffset < 1) {
				byteOffset = 0;
				log.seek(0);
			}

			// read the line
			byte[] lineBytes = new byte[bytesRead.size()];
			int index = 0;
			Iterator<Byte> byteIt = bytesRead.iterator();
			while (byteIt.hasNext()) {
				lineBytes[index] = byteIt.next();
				index++;
			}
			String lastLine = new String(lineBytes, StandardCharsets.UTF_8);

			if (lastLine != null) {
				// is this a log line?
				Matcher matcher = StackTracePatternLayout.DEFAULT_PARSER_PATTERN.matcher(lastLine);
				if (matcher.matches()) {
					try {
						LogLevel level = LogLevel.valueOf(matcher.group(1).trim());
						Date timestamp = LogRecord.ISO8601_TIMESTAMP_FORMAT.parse(matcher.group(2).trim());
						String threadName = matcher.group(3);
						message.insert(0, matcher.group(4));

						result = new SimpleLogRecord();
						result.setLevel(level);
						result.setTime(timestamp);
						result.setThreadName(threadName);
						result.setMessage(message.toString());
						result.setStackTrace(stackTrace);

						message = new StringBuilder();
						stackTrace = new ArrayList<>();
					} catch (ParseException pe) {
						throw new IOException("Unable to parse timestamp in log record");
					}
				}
				// it may be a message line or a stacktrace line
				else {
					if (!lastLine.trim().isEmpty()) {
						if (lastLine.startsWith("\t")) {
							stackTrace.add(0, lastLine.trim());
						} else {
							message.insert(0, lastLine);
						}
					}
				}
			}
		}

		return result;
	}

	@Override
	public void destroy() throws IOException {
		if (log != null) {
			log.close();
		}
		log = null;
	}

}