package org.apache.flume.source.taildirectory; import java.nio.file.*; import java.nio.file.WatchEvent.Kind; import static java.nio.file.StandardWatchEventKinds.*; import static java.nio.file.LinkOption.*; import java.nio.file.attribute.*; import java.io.*; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.flume.Context; import org.apache.flume.Event; import org.apache.flume.event.EventBuilder; import org.apache.flume.source.AbstractSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.regex.*; public class WatchDir { private static final String UNLOCK_TIME = "unlockFileTime"; private static final String FILE_HEADER = "fileHeader"; private static final String BASENAME_HEADER = "basenameHeader"; private static final String FILE_HEADER_KEY = "fileHeaderKey"; private static final String BASENAME_HEADER_KEY = "basenameHeaderKey"; private static final String FOLLOW_LINKS = "followLinks"; private final WatchService watcher; private final Map<WatchKey, Path> keys; private AbstractSource source; private FileSetMap fileSetMap; private Map<String, String> filePathsAndKeys; private long timeToUnlockFile; private DirectoryTailSourceCounter counter; private boolean fileHeader, basenameHeader; private String fileHeaderKey, basenameHeaderKey; private boolean followLinks; private String filenamePattern; private static final Logger LOGGER= LoggerFactory .getLogger(WatchDir.class); private final ScheduledExecutorService scheduler = Executors .newScheduledThreadPool(2); /** * Creates a WatchService and registers the given directory */ WatchDir(Path dir, String filenamePattern, AbstractSource source, Context context, DirectoryTailSourceCounter counter) throws IOException { LOGGER.trace("WatchDir: WatchDir"); this.filenamePattern = filenamePattern; loadConfiguration(context); this.counter = counter; this.source = source; this.watcher = FileSystems.getDefault().newWatchService(); this.keys = new HashMap<WatchKey, Path>(); this.filePathsAndKeys = new HashMap<String, String>(); this.fileSetMap = new FileSetMap(filePathsAndKeys); LOGGER.info("Scanning directory: " + dir); registerAll(dir); Thread t = new Thread(new WatchDirRunnable()); t.start(); final Runnable lastAppend = new CheckLastTimeModified(); scheduler.scheduleAtFixedRate(lastAppend, 0, 1, TimeUnit.MINUTES); final Runnable printThroughput = new PrintThroughput(); scheduler.scheduleAtFixedRate(printThroughput, 0, 5, TimeUnit.SECONDS); } private void loadConfiguration(Context context) { timeToUnlockFile = context.getLong(UNLOCK_TIME, 1L); fileHeader = new Boolean(context.getBoolean(FILE_HEADER, false)); fileHeaderKey = new String(context.getString(FILE_HEADER_KEY, "file")); basenameHeader = new Boolean(context.getBoolean(BASENAME_HEADER, false)); basenameHeaderKey = new String(context.getString(BASENAME_HEADER_KEY, "basename")); followLinks = new Boolean(context.getBoolean(FOLLOW_LINKS, false)); } @SuppressWarnings("unchecked") static <T> WatchEvent<T> cast(WatchEvent<?> event) { return (WatchEvent<T>) event; } /** * Register the given directory, and all its sub-directories, with the * WatchService. */ private void registerAll(final Path start) throws IOException { LOGGER.trace("WatchDir: registerAll"); EnumSet<FileVisitOption> opts; if (followLinks) opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); else opts = EnumSet.noneOf(FileVisitOption.class); Files.walkFileTree(start, opts, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); return FileVisitResult.CONTINUE; } }); // register directory and sub-directories Files.walkFileTree(start, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); return FileVisitResult.CONTINUE; } }); } /** * Register the given directory with the WatchService */ private void register(Path dir) throws IOException { LOGGER.trace("WatchDir: register"); WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); Path prev = keys.get(key); LOGGER.info("Previous directory: " + prev); if (prev == null) { LOGGER.info("Registering directory: " + dir); } else { if (!dir.equals(prev)) { LOGGER.info("Updating previous directory: " + "-> " + prev + " to " + dir); } } keys.put(key, dir); File folder = dir.toFile(); for (final File fileEntry : folder.listFiles()) { if (!fileEntry.isDirectory() && (filenamePattern.isEmpty() != Pattern.matches(filenamePattern, fileEntry.getPath()))) fileSetMap.addFileSetToMap(fileEntry.toPath(), "end"); } } private void readLines(FileSet fileSet) throws IOException{ String buffer; while ((buffer = fileSet.readLine()) != null) { if (buffer.length() == 0) { LOGGER.debug("Readed empty line"); continue; } else { fileSet.appendLine(buffer); if (fileSet.getBufferList().isEmpty()) return; sendEvent(fileSet); } } } private void fileCreated(Path path) throws IOException{ LOGGER.trace("WatchDir: fileCreated"); boolean directory = false; if (followLinks){ if (Files.isDirectory(path)){ registerAll(path); directory=true; } }else if (Files.isDirectory(path, NOFOLLOW_LINKS)){ registerAll(path); directory=true; } if(!directory && (filenamePattern.isEmpty() != Pattern.matches(filenamePattern, path.toString()))){ FileSet fileSet = fileSetMap.addFileSetToMap(path,"begin"); if (fileSet != null && fileSet.isFileIsOpen()) readLines(fileSet); } } private void fileModified(Path path) throws IOException { LOGGER.trace("WatchDir: fileModified"); if(filenamePattern.isEmpty() != Pattern.matches(filenamePattern, path.toString())) { FileSet fileSet = fileSetMap.getFileSet(path); if (fileSet != null) { if (!fileSet.isFileIsOpen()) fileSet.open(); readLines(fileSet); } } } private void fileDeleted(Path path) throws IOException { LOGGER.trace("WatchDir: fileDeleted"); if (filenamePattern.isEmpty() != Pattern.matches(filenamePattern, path.toString())) { String fileKey = FileKeys.getFileKey(path); if (fileKey == null) { fileKey = filePathsAndKeys.get(path.toString()); } if (fileKey != null) { FileSet fileSet = fileSetMap.get(fileKey); if (fileSet.isFileIsOpen()) { fileSet.clear(); fileSet.close(); } if (filePathsAndKeys.containsKey(path.toString())) { filePathsAndKeys.remove(path.toString()); } } } } private void sendEvent(FileSet fileSet) { LOGGER.trace("WatchDir: sendEvent"); if (fileSet.getBufferList().isEmpty()) return; StringBuilder sb = fileSet.getAllLines(); Event event = EventBuilder.withBody(String.valueOf(sb).getBytes(), fileSet.getHeaders()); Map<String,String> headers = new HashMap<String, String>(); if (fileHeader) headers.put(fileHeaderKey,fileSet.getFilePath().toString()); if (basenameHeader) headers.put(basenameHeaderKey, fileSet.getFileName().toString()); if (!headers.isEmpty()) event.setHeaders(headers); source.getChannelProcessor().processEvent(event); counter.increaseCounterMessageSent(); fileSet.clear(); } public void stop() { LOGGER.trace("WatchDir: stop"); try { for (FileSet fileSet : fileSetMap.values()) { LOGGER.debug("Closing file: " + fileSet.getFilePath()); fileSet.clear(); fileSet.close(); } } catch (IOException x) { LOGGER.error(x.getMessage(),x); } } private class CheckLastTimeModified implements Runnable { @Override public void run() { long lastAppendTime, currentTime; FileSet fileSet; try { Set<String> fileKeySet = new HashSet<String>( fileSetMap.keySet()); for (String fileKey : fileKeySet) { fileSet = fileSetMap.get(fileKey); if (fileSet.isFileIsOpen()){ lastAppendTime = fileSet.getLastAppendTime(); currentTime = System.currentTimeMillis(); LOGGER.trace("FILE: {}",fileSet.getFilePath()); Date date = new Date(lastAppendTime); LOGGER.trace("LAST APPEND TIME {}", date); date = new Date(currentTime); LOGGER.trace("CURRENT TIME {}", date); LOGGER.debug("Checking file: " + fileSet.getFilePath()); if (currentTime - lastAppendTime > TimeUnit.MINUTES .toMillis(timeToUnlockFile)) { LOGGER.info("File: " + fileSet.getFilePath() + " not modified after " + timeToUnlockFile + " minutes" + " closing file"); fileSetMap.get(fileKey).clear(); fileSetMap.get(fileKey).close(); } } } } catch (IOException e) { LOGGER.error(e.getMessage(),e); } } } private class PrintThroughput implements Runnable { @Override public void run() { LOGGER.debug("Current throughput: " + counter.getCurrentThroughput()); } } private class WatchDirRunnable implements Runnable { @Override public void run() { try { for (;;) { // wait for key to be signaled WatchKey key; key = watcher.take(); Path dir = keys.get(key); if (dir == null) { LOGGER.error("WatchKey not recognized!!"); continue; } for (WatchEvent<?> event : key.pollEvents()) { try{ Kind<?> kind = event.kind(); // Context for directory entry event is the file name of // entry WatchEvent<Path> ev = cast(event); Path name = ev.context(); Path path = dir.resolve(name); // print out event LOGGER.trace(event.kind().name() + ": " + path); if (kind == ENTRY_MODIFY) { fileModified(path); } else if (kind == ENTRY_CREATE) { fileCreated(path); } else if (kind == ENTRY_DELETE) { fileDeleted(path); } } catch (IOException x) { LOGGER.error(x.getMessage(), x); } } // reset key and remove from set if directory no longer // accessible boolean valid = key.reset(); if (!valid) { keys.remove(key); // all directories are inaccessible if (keys.isEmpty()) { break; } } } } catch (InterruptedException x) { LOGGER.error(x.getMessage(), x); } } } }