// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.container.logging; import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.io.NativeIO; import com.yahoo.log.LogFileDb; import com.yahoo.system.ProcessExecuter; import com.yahoo.yolean.Exceptions; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.StreamHandler; import java.util.zip.GZIPOutputStream; /** *

Implements log file naming/rotating logic for container logs.

* *

Overridden methods: publish

* *

Added methods: setFilePattern, setRotationTimes, rotateNow (+ few others)

* * @author Bob Travis */ public class LogFileHandler extends StreamHandler { private final static Logger logger = Logger.getLogger(LogFileHandler.class.getName()); private final boolean compressOnRotation; private long[] rotationTimes = {0}; //default to one log per day, at midnight private String filePattern = "./log.%T"; // default to current directory, ms time stamp private long nextRotationTime = 0; private FileOutputStream currentOutputStream = null; private volatile String fileName; private String symlinkName = null; private ArrayBlockingQueue logQueue = new ArrayBlockingQueue<>(100000); private LogRecord rotateCmd = new LogRecord(Level.SEVERE, "rotateNow"); private ExecutorService executor = Executors.newCachedThreadPool(ThreadFactoryFactory.getDaemonThreadFactory("logfilehandler.compression")); private final NativeIO nativeIO = new NativeIO(); private long lastDropPosition = 0; static private class LogThread extends Thread { LogFileHandler logFileHandler; long lastFlush = 0; LogThread(LogFileHandler logFile) { super("Logger"); setDaemon(true); logFileHandler = logFile; } @Override public void run() { try { storeLogRecords(); } catch (InterruptedException e) { } catch (Exception e) { com.yahoo.protect.Process.logAndDie("Failed storing log records", e); } logFileHandler.flush(); } private void storeLogRecords() throws InterruptedException { while (!isInterrupted()) { LogRecord r = logFileHandler.logQueue.poll(100, TimeUnit.MILLISECONDS); if (r != null) { if (r == logFileHandler.rotateCmd) { logFileHandler.internalRotateNow(); lastFlush = System.nanoTime(); } else { logFileHandler.internalPublish(r); } flushIfOld(3, TimeUnit.SECONDS); } else { flushIfOld(100, TimeUnit.MILLISECONDS); } } } private void flushIfOld(long age, TimeUnit unit) { long now = System.nanoTime(); if (TimeUnit.NANOSECONDS.toMillis(now - lastFlush) > unit.toMillis(age)) { logFileHandler.flush(); lastFlush = now; } } } private final LogThread logThread; LogFileHandler() { this(false); } LogFileHandler(boolean compressOnRotation) { super(); this.compressOnRotation = compressOnRotation; logThread = new LogThread(this); logThread.start(); } /** * Sends logrecord to file, first rotating file if needed. * * @param r logrecord to publish */ public void publish(LogRecord r) { try { logQueue.put(r); } catch (InterruptedException e) { } } @Override public synchronized void flush() { super.flush(); try { if (currentOutputStream != null) { long newPos = currentOutputStream.getChannel().position(); if (newPos > lastDropPosition + 102400) { nativeIO.dropPartialFileFromCache(currentOutputStream.getFD(), lastDropPosition, newPos, true); lastDropPosition = newPos; } } } catch (IOException e) { logger.warning("Failed dropping from cache : " + Exceptions.toMessageString(e)); } } private void internalPublish(LogRecord r) { // first check to see if new file needed. // if so, use this.internalRotateNow() to do it long now = System.currentTimeMillis(); if (nextRotationTime <= 0) { nextRotationTime = getNextRotationTime(now); // lazy initialization } if (now > nextRotationTime || currentOutputStream == null) { internalRotateNow(); } super.publish(r); } /** * Assign pattern for generating (rotating) file names. * * @param pattern See LogFormatter for definition */ void setFilePattern ( String pattern ) { filePattern = pattern; } /** * Assign times for rotating output files. * * @param timesOfDay in millis, from midnight * */ void setRotationTimes ( long[] timesOfDay ) { rotationTimes = timesOfDay; } /** Assign time for rotating output files * * @param prescription string form of times, in minutes */ void setRotationTimes ( String prescription ) { setRotationTimes(calcTimesMinutes(prescription)); } /** * Find next rotation after specified time. * * @param now the specified time; if zero, current time is used. * @return the next rotation time */ long getNextRotationTime (long now) { if (now <= 0) { now = System.currentTimeMillis(); } long nowTod = timeOfDayMillis(now); long next = 0; for (long rotationTime : rotationTimes) { if (nowTod < rotationTime) { next = rotationTime-nowTod + now; break; } } if (next == 0) { // didn't find one -- use 1st time 'tomorrow' next = rotationTimes[0]+lengthOfDayMillis-nowTod + now; } return next; } void waitDrained() { while(! logQueue.isEmpty()) { try { Thread.sleep(1); } catch (InterruptedException e) { } } flush(); } private void checkAndCreateDir(String pathname) { int lastSlash = pathname.lastIndexOf("/"); if (lastSlash > -1) { String pathExcludingFilename = pathname.substring(0, lastSlash); File filepath = new File(pathExcludingFilename); if (!filepath.exists()) { filepath.mkdirs(); } } } /** * Force file rotation now, independent of schedule. */ void rotateNow () { publish(rotateCmd); } // Throw InterruptedException upwards rather than relying on isInterrupted to stop the thread as // isInterrupted() returns false after interruption in p.waitFor private void internalRotateNow() { // figure out new file name, then // use super.setOutputStream to switch to a new file String oldFileName = fileName; long now = System.currentTimeMillis(); fileName = LogFormatter.insertDate(filePattern, now); flush(); super.close(); try { checkAndCreateDir(fileName); FileOutputStream os = new FileOutputStream(fileName, true); // append mode, for safety super.setOutputStream(os); currentOutputStream = os; lastDropPosition = 0; LogFileDb.nowLoggingTo(fileName); } catch (IOException e) { throw new RuntimeException("Couldn't open log file '" + fileName + "'", e); } createSymlinkToCurrentFile(); nextRotationTime = 0; //figure it out later (lazy evaluation) if ((oldFileName != null)) { File oldFile = new File(oldFileName); if (oldFile.exists()) { if (compressOnRotation) { executor.execute(() -> runCompression(oldFile)); } else { nativeIO.dropFileFromCache(oldFile); } } } } static void runCompression(File oldFile) { File gzippedFile = new File(oldFile.getPath() + ".gz"); try (GZIPOutputStream compressor = new GZIPOutputStream(new FileOutputStream(gzippedFile), 0x100000); FileInputStream inputStream = new FileInputStream(oldFile)) { byte [] buffer = new byte[0x400000]; // 4M buffer long totalBytesRead = 0; NativeIO nativeIO = new NativeIO(); for (int read = inputStream.read(buffer); read > 0; read = inputStream.read(buffer)) { compressor.write(buffer, 0, read); nativeIO.dropPartialFileFromCache(inputStream.getFD(), totalBytesRead, read, false); totalBytesRead += read; } compressor.finish(); compressor.flush(); oldFile.delete(); nativeIO.dropFileFromCache(gzippedFile); } catch (IOException e) { logger.warning("Got '" + e + "' while compressing '" + oldFile.getPath() + "'."); } } /** Name files by date - create a symlink with a constant name to the newest file */ private void createSymlinkToCurrentFile() { if (symlinkName == null) return; File f = new File(fileName); File f2 = new File(f.getParent(), symlinkName); String [] cmd = new String[]{"/bin/ln", "-sf", f.getName(), f2.getPath()}; try { int retval = new ProcessExecuter().exec(cmd).getFirst(); // Detonator pattern: Think of all the fun we can have if ln isn't what we // think it is, if it doesn't return, etc, etc if (retval != 0) { logger.warning("Command '" + Arrays.toString(cmd) + "' + failed with exitcode=" + retval); } } catch (IOException e) { logger.warning("Got '" + e + "' while doing'" + Arrays.toString(cmd) + "'."); } } /** * Calculate rotation times array, given times in minutes, as "0 60 ..." * */ private static long[] calcTimesMinutes(String times) { ArrayList list = new ArrayList<>(50); int i = 0; boolean etc = false; while (i < times.length()) { if (times.charAt(i) == ' ') { i++; continue; } // skip spaces int j = i; // start of string i = times.indexOf(' ', i); if (i == -1) i = times.length(); if (times.charAt(j) == '.' && times.substring(j,i).equals("...")) { // ... etc = true; break; } list.add(Long.valueOf(times.substring(j,i))); } int size = list.size(); long[] longtimes = new long[size]; for (i = 0; i 0) { int newsize = size + (int)moreneeded; long[] temp = new long[newsize]; for (i=0; i