/*
* Copyright © 2014 Cask Data, Inc.
*
* Licensed 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 co.cask.cdap.common.logging;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A LogReader reading from file.
*/
public class LogFileReader implements LogReader {
LogConfiguration config;
FileSystem fileSystem;
@Override
public void configure(LogConfiguration config) throws IOException {
// remember configuration
this.config = config;
// open a file system
fileSystem = config.getFileSystem();
}
@Override
public List<String> tail(int sizeToRead, long writePos) throws IOException {
return tail(new ArrayList<String>(), 0, sizeToRead, writePos);
}
/**
* Recursive method to tail the log. Reads from the current log file
* instance (i), and if that does not have sufficient size, recurses to the
* next older instance (i+1). If the caller knows the size of the current
* file (i), the caller can pass it via the fileSize parameter.
*
* @param lines A list of log lines to append read lines to
* @param i The current log file instance to start reading from
* @param size number of bytes to read at most
* @param sizeHint if known, the caller should pass in the length of the
* current log file instance. This helps to seek to the end
* of a file that has not been closed yet (and hence file
* status does not reflect its correct size). Only needed
* at instance 0. Otherwise (for recursive calls) this is
* -1, and the file size will be obatained via file status.
* @return The list of lines read
* @throws IOException if reading goes badly wrong
*/
private List<String> tail(ArrayList<String> lines, int i, long size,
long sizeHint)
throws IOException {
// get the path of the current log file instance (xxx.log[.i])
Path path = new Path(config.getLogFilePath(), makeFileName(i));
// check for its existence, if it does not exist, return empty list
if (!fileSystem.exists(path)) {
return lines;
}
FileStatus status = fileSystem.getFileStatus(path);
if (!status.isFile()) {
return lines;
}
long fileSize;
if (sizeHint >= 0) {
fileSize = sizeHint;
} else if (i > 0) {
fileSize = status.getLen();
} else {
fileSize = determineTrueFileSize(path, status);
}
long seekPos = 0;
long bytesToRead = size;
if (fileSize >= size) {
// if size of currentFile is sufficient, we need to seek to the
// position that is size bytes from the end of the file.
seekPos = fileSize - size;
} else {
// if size of current file is less than limit, make a recursive
// call to tail for previous file
tail(lines, i + 1, size - fileSize, -1);
bytesToRead = fileSize;
}
// open current file for reading
byte[] bytes = new byte[(int) bytesToRead];
try (FSDataInputStream input = fileSystem.open(path)) {
// seek into latest file
if (seekPos > 0) {
input.seek(seekPos);
}
// read to the end of current file
input.readFully(bytes);
}
int pos = 0;
if (seekPos > 0) {
// if we seeked into the file, then we are likely in the middle of the
// line, and we want to skip up to the first new line
while (pos < bytesToRead && bytes[pos] != '\n') {
pos++;
}
pos++; // now we are just after the first new line
}
// read lines until the end of the buffer
while (pos < bytesToRead) {
int start = pos;
while (pos < bytesToRead && bytes[pos] != '\n') {
pos++;
}
// now we are at end of file or at the new line
if (pos != start) { // ignore empty lines
String line = new String(bytes, start, pos - start,
LogFileWriter.CHARSET_UTF8);
lines.add(line);
}
pos++; // skip the new line character
}
return lines;
}
private long determineTrueFileSize(Path path, FileStatus status)
throws IOException {
try (FSDataInputStream stream = fileSystem.open(path)) {
stream.seek(status.getLen());
// we need to read repeatedly until we reach the end of the file
byte[] buffer = new byte[1024 * 1024];
while (stream.read(buffer, 0, buffer.length) >= 0) {
// empty body.
}
long trueSize = stream.getPos();
return trueSize;
}
}
String makeFileName(int instance) {
if (instance == 0) {
return config.getLogFileName();
} else {
return String.format("%s.%d", config.getLogFileName(), instance);
}
}
}