package chatty.util.chatlog; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.logging.Logger; /** * Handles writing the log files. Retrieves data from a queue and manages files * to write the log into. * * @author tduva */ public class LogWriter implements Runnable { private static final Logger LOGGER = Logger.getLogger(LogWriter.class.getName()); private static final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZ"); private static final int STATS_INTERVAL = 500; private static final int STATS_TIME_INTERVAL = 5 * 60 * 1000; private final Map<String, LogFile> files = new HashMap<>(); private final Set<String> errors = new HashSet<>(); private final BlockingQueue<LogItem> queue; private final Path path; private final String splitLogs; private final boolean useSubdirectories; private final boolean lockFiles; private long addedQueueSize; private int addedQueueSizeCount; private int errorCount; private long lastStatsTime; private int maxQueueSize; private int totalLines; public LogWriter(BlockingQueue<LogItem> queue, Path path, String splitLogs, boolean useSubdirectories, boolean lockFiles) { this.queue = queue; this.path = path; this.splitLogs = splitLogs; this.useSubdirectories = useSubdirectories; this.lockFiles = lockFiles; } @Override public void run() { boolean run = true; try { while (run) { //System.out.println("Waiting for a new item.."); LogItem item = queue.take(); stats(queue.size()); if (item.channel == null) { if (item.message == null) { outputStats(); run = false; closeAllFiles(); } else { // Can't close any files here because it would // remove an item during iteration for (String channel : files.keySet()) { handleMessage(channel, item.message); } } } else { handleMessage(item.channel, item.message); } } } catch (InterruptedException ex) { System.out.println("Interrupted"); closeAllFiles(); Thread.currentThread().interrupt(); } } private void closeAllFiles() { for (String channel : files.keySet()) { LogFile file = files.get(channel); closeFile(file); } files.clear(); } private void handleMessage(String channel, String message) { if (message == null) { closeFileForChannel(channel); } else { writeLine(channel, message); } } private void writeLine(String channel, String line) { LogFile file = getFile(channel); if (file == null || !file.write(line)) { fileError(channel); } } private LogFile getFile(String channel) { LogFile file = files.get(channel); String datePrefix = ""; if (!splitLogs.equals("never")) { datePrefix = getDatePrefix() + "_"; } if (file != null && file.isValid()) { if (!datePrefix.isEmpty() && shouldSplitLog(file.getDate())) { file.close(); return addFile(channel, datePrefix); } return file; } if (errors.contains(channel)) { return null; } return addFile(channel, datePrefix); } /** * Get the date prefix for the log filenames, based on the splitLogs * setting. * * @return The date String to prefix log filenames with, or an empty String * if the splitLogs setting does not have a valid value */ private String getDatePrefix() { Calendar date = Calendar.getInstance(); if (splitLogs.equals("daily")) { date.set(Calendar.HOUR_OF_DAY, 0); date.clear(Calendar.MINUTE); date.clear(Calendar.SECOND); date.clear(Calendar.MILLISECOND); return new SimpleDateFormat("yyyy-MM-dd").format(date.getTime()); } else if (splitLogs.equals("weekly")) { date.set(Calendar.DAY_OF_WEEK, date.getFirstDayOfWeek()); return new SimpleDateFormat("yyyy-MM-dd").format(date.getTime()); } else if (splitLogs.equals("monthly")) { date.set(Calendar.DAY_OF_MONTH, 1); return new SimpleDateFormat("yyyy-MM-dd").format(date.getTime()); } return ""; } /** * Determine if we should split the log file before writing. * * @param fileDate The date that the LogFile instance was created * @return Returns true if the log file should be split before writing */ private boolean shouldSplitLog(Calendar fileDate) { Calendar date = Calendar.getInstance(); if (splitLogs.equals("daily")) { if (fileDate.get(Calendar.DAY_OF_YEAR) != date.get(Calendar.DAY_OF_YEAR)) { return true; } } else if (splitLogs.equals("weekly")) { if (fileDate.get(Calendar.WEEK_OF_YEAR) != date.get(Calendar.WEEK_OF_YEAR)) { return true; } } else if (splitLogs.equals("monthly")) { if (fileDate.get(Calendar.MONTH) != date.get(Calendar.MONTH)) { return true; } } return false; } private LogFile addFile(String channel, String datePrefix) { Path channelPath = path; if (useSubdirectories) { channelPath = channelPath.resolve(channel); channelPath.toFile().mkdirs(); if (!channelPath.toFile().exists()) { LOGGER.warning("Log: Failed to create path: " + channelPath); } } LogFile file = LogFile.get(channelPath, datePrefix + channel, lockFiles); if (file == null) { errors.add(channel); } else { files.put(channel, file); file.write("# Log started: " + getDateTime()); LOGGER.info("Log: Opened file " + file.getPath()+(file.isLocked() ? " (locked)" : "")); } return file; } private void fileError(String channel) { //LOGGER.warning("LOG: Could not write to file for "+channel); files.remove(channel); errors.add(channel); errorCount++; } private void closeFileForChannel(String channel) { LogFile file = files.get(channel); closeFile(file); files.remove(channel); } private void closeFile(LogFile file) { if (file != null && file.isValid()) { file.write("# Log closed: " + getDateTime()); file.write("-"); file.close(); } } private String getDateTime() { Calendar cal = Calendar.getInstance(); return dateTimeFormat.format(cal.getTime()); } private void stats(int size) { addedQueueSize += size; addedQueueSizeCount++; totalLines++; if (maxQueueSize < size) { maxQueueSize = size; } long lastStatsAgo = System.currentTimeMillis() - lastStatsTime; if (addedQueueSizeCount > STATS_INTERVAL || lastStatsAgo > STATS_TIME_INTERVAL) { outputStats(); } } private void outputStats() { long avg = addedQueueSizeCount > 0 ? addedQueueSize / addedQueueSizeCount : 0; LOGGER.info("Log: total: " + totalLines + " / queue size (avg: " + avg + ", max: " + maxQueueSize + ") / errors: " + errorCount); addedQueueSize = 0; addedQueueSizeCount = 0; errorCount = 0; maxQueueSize = 0; lastStatsTime = System.currentTimeMillis(); } public static class LogItem { public final String channel; public final String message; public LogItem(String channel, String message) { this.channel = channel; this.message = message; } } }