/**
* Copyright 2014 Eediom 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 org.araqne.log.api.nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.araqne.log.api.AbstractLogPipe;
import org.araqne.log.api.AbstractLogger;
import org.araqne.log.api.LastPosition;
import org.araqne.log.api.LastPositionHelper;
import org.araqne.log.api.Log;
import org.araqne.log.api.Logger;
import org.araqne.log.api.LoggerFactory;
import org.araqne.log.api.LoggerSpecification;
import org.araqne.log.api.LoggerStartReason;
import org.araqne.log.api.LoggerStatus;
import org.araqne.log.api.LoggerStopReason;
import org.araqne.log.api.MultilineLogExtractor;
import org.araqne.log.api.Reconfigurable;
public class NioRecursiveDirectoryWatchLogger extends AbstractLogger implements Reconfigurable {
private final org.slf4j.Logger slog = org.slf4j.LoggerFactory.getLogger(NioRecursiveDirectoryWatchLogger.class.getName());
private int pollInterval = 0;
private int pollTimeout = 100;
private String basePath;
private Pattern fileNamePattern;
private Pattern dirPathPattern;
private boolean recursive;
private String fileTag;
private Receiver receiver = new Receiver();
/**
* NOTE: must be separate thread for accurate event processing
*/
private ChangeDetector detector;
private MultilineLogExtractor extractor;
private boolean walkTreeRequired = true;
private boolean walkForceStopped = false;
// update only state is modified (reduce large file set serialization
// overhead)
private volatile boolean modifiedStates;
public NioRecursiveDirectoryWatchLogger(LoggerSpecification spec, LoggerFactory factory) {
super(spec, factory);
loadPollConfigs();
if (slog.isDebugEnabled())
slog.debug("araqne-logapi-nio: recursive dirwatcher uses nio");
applyConfig();
}
private void loadPollConfigs() {
String s = System.getProperty("araqne.nio.poll_interval");
if (s != null) {
try {
pollInterval = Integer.valueOf(s);
slog.info("araqne-logapi-nio: use recursive watcher poll interval [{}]", pollInterval);
} catch (Throwable t) {
}
}
s = System.getProperty("araqne.nio.poll_timeout");
if (s != null) {
try {
pollTimeout = Integer.valueOf(s);
slog.info("araqne-logapi-nio: use recursive watcher poll timeout [{}]", pollTimeout);
} catch (Throwable t) {
}
}
}
@Override
protected void onResetStates() {
walkTreeRequired = true;
slog.debug("araqne-logapi-nio: recursive-dirwatch [{}] will retraverse directories", getFullName());
}
private void applyConfig() {
basePath = getConfigs().get("base_path");
String fileNameRegex = getConfigs().get("filename_pattern");
fileNamePattern = Pattern.compile(fileNameRegex);
// optional
String dirNameRegex = getConfigs().get("dirpath_pattern");
if (dirNameRegex != null)
dirPathPattern = Pattern.compile(dirNameRegex);
extractor = new MultilineLogExtractor(this, receiver);
// optional
String dateExtractRegex = getConfigs().get("date_pattern");
if (dateExtractRegex != null)
extractor.setDateMatcher(Pattern.compile(dateExtractRegex).matcher(""));
// optional
String dateLocale = getConfigs().get("date_locale");
if (dateLocale == null)
dateLocale = "en";
// optional
String dateFormatString = getConfigs().get("date_format");
String timeZone = getConfigs().get("timezone");
if (dateFormatString != null)
extractor.setDateFormat(new SimpleDateFormat(dateFormatString, new Locale(dateLocale)), timeZone);
// optional
String newlogRegex = getConfigs().get("newlog_designator");
if (newlogRegex != null)
extractor.setBeginMatcher(Pattern.compile(newlogRegex).matcher(""));
String newlogEndRegex = getConfigs().get("newlog_end_designator");
if (newlogEndRegex != null)
extractor.setEndMatcher(Pattern.compile(newlogEndRegex).matcher(""));
// optional
String charset = getConfigs().get("charset");
if (charset == null)
charset = "utf-8";
// optional
String recursive = getConfigs().get("recursive");
this.recursive = ((recursive != null) && (recursive.compareToIgnoreCase("true") == 0));
// optional
this.fileTag = getConfigs().get("file_tag");
extractor.setCharset(charset);
}
@Override
protected void onStart(LoggerStartReason reason) {
detector = new ChangeDetector();
detector.start();
}
@Override
protected void onStop(LoggerStopReason reason) {
detector.close();
walkTreeRequired = true;
}
@Override
protected void runOnce() {
if (detector.deadThread)
throw new IllegalStateException("dead file watcher, logger[ [" + getFullName() + "]");
walkForceStopped = false;
if (walkTreeRequired) {
try {
Map<String, LastPosition> lastPositions = LastPositionHelper.deserialize(getStates());
Path root = FileSystems.getDefault().getPath(basePath);
Files.walkFileTree(root, new InitialRunner(root, lastPositions));
// mark deleted files
for (String path : new ArrayList<String>(lastPositions.keySet())) {
markDeletedFile(lastPositions, new File(path));
}
if (modifiedStates)
setStates(LastPositionHelper.serialize(lastPositions));
else
slog.debug("araqne-logapi-nio: logger [{}] has no modification, skip setStates()", getFullName());
if (!walkForceStopped)
walkTreeRequired = false;
} catch (IOException e) {
throw new IllegalStateException("failed to initial run, logger [" + getFullName() + "]", e);
}
}
Map<String, LastPosition> lastPositions = null;
try {
List<File> changedFiles = new ArrayList<File>(detector.getChangedFiles());
List<File> deletedFiles = new ArrayList<File>(detector.getDeletedFiles());
if (changedFiles.isEmpty() && deletedFiles.isEmpty()) {
return;
}
lastPositions = LastPositionHelper.deserialize(getStates());
Collections.sort(changedFiles);
for (File f : changedFiles) {
processFile(lastPositions, f);
}
Collections.sort(deletedFiles);
for (File f : deletedFiles) {
markDeletedFile(lastPositions, f);
}
} finally {
if (lastPositions != null && modifiedStates)
setStates(LastPositionHelper.serialize(lastPositions));
else
slog.debug("araqne-logapi-nio: logger [{}] has no modification, skip setStates()", getFullName());
modifiedStates = false;
}
}
private void markDeletedFile(Map<String, LastPosition> lastPositions, File f) {
if (f.exists())
return;
String path = f.getAbsolutePath();
LastPosition lp = lastPositions.get(path);
if (lp == null)
return;
modifiedStates = true;
if (lp.getLastSeen() == null) {
lp.setLastSeen(new Date());
slog.debug("araqne-logapi-nio: logger [{}] marked deleted file [{}] state", getFullName(), f.getAbsolutePath());
} else {
long limitTime = lp.getLastSeen().getTime() + 3600000L;
if (limitTime <= System.currentTimeMillis()) {
lastPositions.remove(path);
slog.debug("araqne-logapi-nio: logger [{}] removed deleted file [{}] from states", getFullName(),
f.getAbsolutePath());
}
}
}
protected void processFile(Map<String, LastPosition> lastPositions, File file) {
if (!file.canRead()) {
slog.debug("araqne-api-nio: cannot read file [{}], logger [{}]", file.getAbsolutePath(), getFullName());
return;
}
String path = file.getAbsolutePath();
FileInputStream is = null;
try {
String dateFromFileName = getDateString(file);
// skip previous read part
long offset = 0;
if (lastPositions.containsKey(path)) {
LastPosition inform = lastPositions.get(path);
offset = inform.getPosition();
slog.trace("araqne-logapi-nio: target file [{}] skip offset [{}]", path, offset);
}
if (file.length() <= offset)
return;
modifiedStates = true;
AtomicLong lastPosition = new AtomicLong(offset);
receiver.filename = file.getName();
is = new FileInputStream(file);
is.skip(offset);
extractor.extract(is, lastPosition, dateFromFileName);
slog.debug("araqne-logapi-nio: updating file [{}] old position [{}] new last position [{}]", new Object[] { path,
offset, lastPosition.get() });
LastPosition inform = lastPositions.get(path);
if (inform == null) {
inform = new LastPosition(path);
}
inform.setPosition(lastPosition.get());
lastPositions.put(path, inform);
} catch (FileNotFoundException e) {
if (slog.isTraceEnabled())
slog.trace("araqne-logapi-nio: logger [" + getFullName() + "] read failure: file not found: {}", e.getMessage());
} catch (Throwable e) {
slog.error("araqne-logapi-nio: logger [" + getFullName() + "] read error", e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
}
}
}
private String getDateString(File f) {
StringBuilder sb = new StringBuilder(f.getAbsolutePath().length());
String dirPath = f.getParentFile().getAbsolutePath();
if (dirPathPattern != null) {
Matcher dirNameDateMatcher = dirPathPattern.matcher(dirPath);
while (dirNameDateMatcher.find()) {
int dirNameGroupCount = dirNameDateMatcher.groupCount();
if (dirNameGroupCount > 0) {
for (int i = 1; i <= dirNameGroupCount; ++i) {
sb.append(dirNameDateMatcher.group(i));
}
}
}
}
String fileName = f.getName();
Matcher fileNameDateMatcher = fileNamePattern.matcher(fileName);
while (fileNameDateMatcher.find()) {
int fileNameGroupCount = fileNameDateMatcher.groupCount();
if (fileNameGroupCount > 0) {
for (int i = 1; i <= fileNameGroupCount; ++i) {
sb.append(fileNameDateMatcher.group(i));
}
}
}
String date = sb.toString();
return date.isEmpty() ? null : date;
}
private class Receiver extends AbstractLogPipe {
private String filename;
@Override
public void onLog(Logger logger, Log log) {
if (fileTag != null)
log.getParams().put(fileTag, filename);
write(log);
}
@Override
public void onLogBatch(Logger logger, Log[] logs) {
if (fileTag != null) {
for (Log log : logs) {
log.getParams().put(fileTag, filename);
}
}
writeBatch(logs);
}
}
private class InitialRunner implements FileVisitor<Path> {
private Path root;
private Map<String, LastPosition> lastPositions;
public InitialRunner(Path root, Map<String, LastPosition> lastPositions) {
this.root = root;
this.lastPositions = lastPositions;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
LoggerStatus status = getStatus();
if (status == LoggerStatus.Stopping || status == LoggerStatus.Stopped) {
slog.debug("araqne log api: stop logger [{}] initial runner.", getFullName());
walkForceStopped = true;
return FileVisitResult.TERMINATE;
}
if (!root.equals(dir) && !recursive) {
slog.debug("araqne-logapi-nio: logger [{}] skip directory [{}]", getFullName(), dir.toFile().getAbsolutePath());
return FileVisitResult.SKIP_SUBTREE;
}
slog.debug("araqne-logapi-nio: logger [{}] visit directory [{}]", getFullName(), dir.toFile().getAbsolutePath());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
LoggerStatus status = getStatus();
if (status == LoggerStatus.Stopping || status == LoggerStatus.Stopped) {
slog.debug("araqne log api: stop logger [{}] initial runner.", getFullName());
walkForceStopped = true;
return FileVisitResult.TERMINATE;
}
slog.debug("araqne-logapi-nio: logger [{}] visit file [{}]", getFullName(), file.toFile().getAbsolutePath());
File f = file.toFile();
if (dirPathPattern != null && !dirPathPattern.matcher(f.getParentFile().getAbsolutePath()).find()) {
slog.debug("araqne-logapi-nio: logger [{}] skip file [{}]", getFullName(), f.getAbsolutePath());
walkForceStopped = true;
return FileVisitResult.CONTINUE;
}
if (fileNamePattern.matcher(f.getName()).matches())
processFile(lastPositions, f);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
LoggerStatus status = getStatus();
if (status == LoggerStatus.Stopping || status == LoggerStatus.Stopped) {
slog.debug("araqne log api: stop logger [{}] initial runner.", getFullName());
walkForceStopped = true;
return FileVisitResult.TERMINATE;
}
slog.debug("araqne-logapi-nio: logger [{}] visit file failed [{}]", getFullName(), exc.getMessage());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
LoggerStatus status = getStatus();
if (status == LoggerStatus.Stopping || status == LoggerStatus.Stopped) {
slog.debug("araqne log api: stop logger [{}] initial runner.", getFullName());
walkForceStopped = true;
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
}
private class ChangeDetector extends Thread implements FileEventListener {
private FileEventWatcher evtWatcher;
private Set<File> changedFiles = new HashSet<File>();
private Set<File> deletedFiles = new HashSet<File>();
private volatile boolean doStop;
private volatile boolean deadThread;
private Object changedLock = new Object();
private Object deletedLock = new Object();
public ChangeDetector() {
super("File Watcher [" + getFullName() + "]");
}
public Set<File> getChangedFiles() {
synchronized (changedLock) {
Set<File> copied = new HashSet<File>(changedFiles);
changedFiles.clear();
return copied;
}
}
public Set<File> getDeletedFiles() {
synchronized (deletedLock) {
Set<File> copied = new HashSet<File>(deletedFiles);
deletedFiles.clear();
return copied;
}
}
@Override
public void run() {
try {
slog.info("araqne-logapi-nio: starting file watcher for logger [{}]", getFullName());
this.evtWatcher = new FileEventWatcher(basePath, fileNamePattern, recursive);
evtWatcher.addListener(detector);
while (!doStop) {
evtWatcher.poll(pollTimeout);
if (pollInterval > 0) {
try {
Thread.sleep(pollInterval);
} catch (InterruptedException e) {
slog.debug("araqne-logapi-nio: watcher poll sleep interrupted");
}
}
}
} catch (IOException e) {
slog.error("araqne-logapi-nio: cannot poll file event for logger [" + getFullName() + "]", e);
} finally {
evtWatcher.removeListener(this);
evtWatcher.close();
slog.info("araqne-logapi-nio: stopping file watcher for logger [{}]", getFullName());
deadThread = true;
}
}
public void close() {
doStop = true;
slog.debug("araqne-logapi-nio: closing change detector of logger [{}]", getFullName());
}
@Override
public void onCreate(File file) {
if (file.isFile() && dirPathPattern != null && !dirPathPattern.matcher(file.getParentFile().getAbsolutePath()).find())
return;
synchronized (changedLock) {
changedFiles.add(file);
}
if (slog.isDebugEnabled())
slog.debug("araqne-logapi-nio: logger [{}] detect created file [{}]", getFullName(), file.getAbsolutePath());
}
@Override
public void onDelete(File file) {
synchronized (changedLock) {
changedFiles.remove(file);
}
synchronized (deletedLock) {
deletedFiles.add(file);
}
if (slog.isDebugEnabled())
slog.debug("araqne-logapi-nio: logger [{}] detect deleted file [{}]", getFullName(), file.getAbsolutePath());
}
@Override
public void onModify(File file) {
if (file.isFile() && dirPathPattern != null && !dirPathPattern.matcher(file.getParentFile().getAbsolutePath()).find())
return;
synchronized (changedLock) {
changedFiles.add(file);
}
if (slog.isDebugEnabled())
slog.debug("araqne-logapi-nio: logger [{}] detect modified file [{}]", getFullName(), file.getAbsolutePath());
}
}
@Override
public void onConfigChange(Map<String, String> oldConfigs, Map<String, String> newConfigs) {
if (isRunning())
throw new IllegalStateException("logger is running");
if (!oldConfigs.get("base_path").equals(newConfigs.get("base_path"))
|| !oldConfigs.get("filename_pattern").equals(newConfigs.get("filename_pattern"))) {
setStates(new HashMap<String, Object>());
}
applyConfig();
}
}