| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.juli; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.sql.Timestamp; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ThreadFactory; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import java.util.logging.ErrorManager; |
| import java.util.logging.Filter; |
| import java.util.logging.Formatter; |
| import java.util.logging.Handler; |
| import java.util.logging.Level; |
| import java.util.logging.LogManager; |
| import java.util.logging.LogRecord; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Implementation of <b>Handler</b> that appends log messages to a file |
| * named {prefix}{date}{suffix} in a configured directory. |
| * |
| * <p>The following configuration properties are available:</p> |
| * |
| * <ul> |
| * <li><code>directory</code> - The directory where to create the log file. |
| * If the path is not absolute, it is relative to the current working |
| * directory of the application. The Apache Tomcat configuration files usually |
| * specify an absolute path for this property, |
| * <code>${catalina.base}/logs</code> |
| * Default value: <code>logs</code></li> |
| * <li><code>rotatable</code> - If <code>true</code>, the log file will be |
| * rotated on the first write past midnight and the filename will be |
| * <code>{prefix}{date}{suffix}</code>, where date is yyyy-MM-dd. If <code>false</code>, |
| * the file will not be rotated and the filename will be <code>{prefix}{suffix}</code>. |
| * Default value: <code>true</code></li> |
| * <li><code>prefix</code> - The leading part of the log file name. |
| * Default value: <code>juli.</code></li> |
| * <li><code>suffix</code> - The trailing part of the log file name. Default value: <code>.log</code></li> |
| * <li><code>bufferSize</code> - Configures buffering. The value of <code>0</code> |
| * uses system default buffering (typically an 8K buffer will be used). A |
| * value of <code><0</code> forces a writer flush upon each log write. A |
| * value <code>>0</code> uses a BufferedOutputStream with the defined |
| * value but note that the system default buffering will also be |
| * applied. Default value: <code>-1</code></li> |
| * <li><code>encoding</code> - Character set used by the log file. Default value: |
| * empty string, which means to use the system default character set.</li> |
| * <li><code>level</code> - The level threshold for this Handler. See the |
| * <code>java.util.logging.Level</code> class for the possible levels. |
| * Default value: <code>ALL</code></li> |
| * <li><code>filter</code> - The <code>java.util.logging.Filter</code> |
| * implementation class name for this Handler. Default value: unset</li> |
| * <li><code>formatter</code> - The <code>java.util.logging.Formatter</code> |
| * implementation class name for this Handler. Default value: |
| * <code>java.util.logging.SimpleFormatter</code></li> |
| * <li><code>maxDays</code> - The maximum number of days to keep the log files. |
| * If the specified value is <code><=0</code> then the log files will be kept |
| * on the file system forever, otherwise they will be kept the specified maximum |
| * days. Default value: <code>-1</code>.</li> |
| * </ul> |
| */ |
| public class FileHandler extends Handler { |
| public static final int DEFAULT_MAX_DAYS = -1; |
| |
| private static final ExecutorService DELETE_FILES_SERVICE = |
| Executors.newSingleThreadExecutor(new ThreadFactory() { |
| private final boolean isSecurityEnabled; |
| private final ThreadGroup group; |
| private final AtomicInteger threadNumber = new AtomicInteger(1); |
| private final String namePrefix = "FileHandlerLogFilesCleaner-"; |
| |
| { |
| SecurityManager s = System.getSecurityManager(); |
| if (s == null) { |
| this.isSecurityEnabled = false; |
| this.group = Thread.currentThread().getThreadGroup(); |
| } else { |
| this.isSecurityEnabled = true; |
| this.group = s.getThreadGroup(); |
| } |
| } |
| |
| @Override |
| public Thread newThread(Runnable r) { |
| final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| try { |
| // Threads should not be created by the webapp classloader |
| if (isSecurityEnabled) { |
| AccessController.doPrivileged(new PrivilegedAction<Void>() { |
| |
| @Override |
| public Void run() { |
| Thread.currentThread() |
| .setContextClassLoader(getClass().getClassLoader()); |
| return null; |
| } |
| }); |
| } else { |
| Thread.currentThread() |
| .setContextClassLoader(getClass().getClassLoader()); |
| } |
| Thread t = new Thread(group, r, |
| namePrefix + threadNumber.getAndIncrement()); |
| t.setDaemon(true); |
| return t; |
| } finally { |
| if (isSecurityEnabled) { |
| AccessController.doPrivileged(new PrivilegedAction<Void>() { |
| |
| @Override |
| public Void run() { |
| Thread.currentThread().setContextClassLoader(loader); |
| return null; |
| } |
| }); |
| } else { |
| Thread.currentThread().setContextClassLoader(loader); |
| } |
| } |
| } |
| }); |
| |
| // ------------------------------------------------------------ Constructor |
| |
| |
| public FileHandler() { |
| this(null, null, null, DEFAULT_MAX_DAYS); |
| } |
| |
| |
| public FileHandler(String directory, String prefix, String suffix) { |
| this(directory, prefix, suffix, DEFAULT_MAX_DAYS); |
| } |
| |
| public FileHandler(String directory, String prefix, String suffix, int maxDays) { |
| this.directory = directory; |
| this.prefix = prefix; |
| this.suffix = suffix; |
| this.maxDays = maxDays; |
| configure(); |
| openWriter(); |
| clean(); |
| } |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| |
| /** |
| * The as-of date for the currently open log file, or a zero-length |
| * string if there is no open log file. |
| */ |
| private volatile String date = ""; |
| |
| |
| /** |
| * The directory in which log files are created. |
| */ |
| private String directory = null; |
| |
| |
| /** |
| * The prefix that is added to log file filenames. |
| */ |
| private String prefix = null; |
| |
| |
| /** |
| * The suffix that is added to log file filenames. |
| */ |
| private String suffix = null; |
| |
| |
| /** |
| * Determines whether the log file is rotatable |
| */ |
| private boolean rotatable = true; |
| |
| |
| /** |
| * Maximum number of days to keep the log files |
| */ |
| private int maxDays = DEFAULT_MAX_DAYS; |
| |
| |
| /** |
| * The PrintWriter to which we are currently logging, if any. |
| */ |
| private volatile PrintWriter writer = null; |
| |
| |
| /** |
| * Lock used to control access to the writer. |
| */ |
| protected final ReadWriteLock writerLock = new ReentrantReadWriteLock(); |
| |
| |
| /** |
| * Log buffer size. |
| */ |
| private int bufferSize = -1; |
| |
| |
| /** |
| * Represents a file name pattern of type {prefix}{date}{suffix}. The date |
| * is YYYY-MM-DD |
| */ |
| private Pattern pattern; |
| |
| |
| // --------------------------------------------------------- Public Methods |
| |
| |
| /** |
| * Format and publish a <tt>LogRecord</tt>. |
| * |
| * @param record description of the log event |
| */ |
| @Override |
| public void publish(LogRecord record) { |
| |
| if (!isLoggable(record)) { |
| return; |
| } |
| |
| // Construct the timestamp we will use, if requested |
| Timestamp ts = new Timestamp(System.currentTimeMillis()); |
| String tsString = ts.toString().substring(0, 19); |
| String tsDate = tsString.substring(0, 10); |
| |
| writerLock.readLock().lock(); |
| try { |
| // If the date has changed, switch log files |
| if (rotatable && !date.equals(tsDate)) { |
| // Upgrade to writeLock before we switch |
| writerLock.readLock().unlock(); |
| writerLock.writeLock().lock(); |
| try { |
| // Make sure another thread hasn't already done this |
| if (!date.equals(tsDate)) { |
| closeWriter(); |
| date = tsDate; |
| openWriter(); |
| clean(); |
| } |
| } finally { |
| // Downgrade to read-lock. This ensures the writer remains valid |
| // until the log message is written |
| writerLock.readLock().lock(); |
| writerLock.writeLock().unlock(); |
| } |
| } |
| |
| String result = null; |
| try { |
| result = getFormatter().format(record); |
| } catch (Exception e) { |
| reportError(null, e, ErrorManager.FORMAT_FAILURE); |
| return; |
| } |
| |
| try { |
| if (writer != null) { |
| writer.write(result); |
| if (bufferSize < 0) { |
| writer.flush(); |
| } |
| } else { |
| reportError("FileHandler is closed or not yet initialized, unable to log [" + result + "]", null, |
| ErrorManager.WRITE_FAILURE); |
| } |
| } catch (Exception e) { |
| reportError(null, e, ErrorManager.WRITE_FAILURE); |
| return; |
| } |
| } finally { |
| writerLock.readLock().unlock(); |
| } |
| } |
| |
| |
| // -------------------------------------------------------- Private Methods |
| |
| |
| /** |
| * Close the currently open log file (if any). |
| */ |
| @Override |
| public void close() { |
| closeWriter(); |
| } |
| |
| protected void closeWriter() { |
| |
| writerLock.writeLock().lock(); |
| try { |
| if (writer == null) { |
| return; |
| } |
| writer.write(getFormatter().getTail(this)); |
| writer.flush(); |
| writer.close(); |
| writer = null; |
| date = ""; |
| } catch (Exception e) { |
| reportError(null, e, ErrorManager.CLOSE_FAILURE); |
| } finally { |
| writerLock.writeLock().unlock(); |
| } |
| } |
| |
| |
| /** |
| * Flush the writer. |
| */ |
| @Override |
| public void flush() { |
| |
| writerLock.readLock().lock(); |
| try { |
| if (writer == null) { |
| return; |
| } |
| writer.flush(); |
| } catch (Exception e) { |
| reportError(null, e, ErrorManager.FLUSH_FAILURE); |
| } finally { |
| writerLock.readLock().unlock(); |
| } |
| |
| } |
| |
| |
| /** |
| * Configure from <code>LogManager</code> properties. |
| */ |
| private void configure() { |
| |
| Timestamp ts = new Timestamp(System.currentTimeMillis()); |
| String tsString = ts.toString().substring(0, 19); |
| date = tsString.substring(0, 10); |
| |
| String className = this.getClass().getName(); //allow classes to override |
| |
| ClassLoader cl = Thread.currentThread().getContextClassLoader(); |
| |
| // Retrieve configuration of logging file name |
| rotatable = Boolean.parseBoolean(getProperty(className + ".rotatable", "true")); |
| if (directory == null) { |
| directory = getProperty(className + ".directory", "logs"); |
| } |
| if (prefix == null) { |
| prefix = getProperty(className + ".prefix", "juli."); |
| } |
| if (suffix == null) { |
| suffix = getProperty(className + ".suffix", ".log"); |
| } |
| |
| // https://bz.apache.org/bugzilla/show_bug.cgi?id=61232 |
| boolean shouldCheckForRedundantSeparator = !rotatable && !prefix.isEmpty() |
| && !suffix.isEmpty(); |
| // assuming separator is just one char, if there are use cases with |
| // more, the notion of separator might be introduced |
| if (shouldCheckForRedundantSeparator |
| && (prefix.charAt(prefix.length() - 1) == suffix.charAt(0))) { |
| suffix = suffix.substring(1); |
| } |
| |
| pattern = Pattern.compile("^(" + Pattern.quote(prefix) + ")\\d{4}-\\d{1,2}-\\d{1,2}(" |
| + Pattern.quote(suffix) + ")$"); |
| String sMaxDays = getProperty(className + ".maxDays", String.valueOf(DEFAULT_MAX_DAYS)); |
| if (maxDays <= 0) { |
| try { |
| maxDays = Integer.parseInt(sMaxDays); |
| } catch (NumberFormatException ignore) { |
| // no-op |
| } |
| } |
| String sBufferSize = getProperty(className + ".bufferSize", String.valueOf(bufferSize)); |
| try { |
| bufferSize = Integer.parseInt(sBufferSize); |
| } catch (NumberFormatException ignore) { |
| //no op |
| } |
| // Get encoding for the logging file |
| String encoding = getProperty(className + ".encoding", null); |
| if (encoding != null && encoding.length() > 0) { |
| try { |
| setEncoding(encoding); |
| } catch (UnsupportedEncodingException ex) { |
| // Ignore |
| } |
| } |
| |
| // Get logging level for the handler |
| setLevel(Level.parse(getProperty(className + ".level", "" + Level.ALL))); |
| |
| // Get filter configuration |
| String filterName = getProperty(className + ".filter", null); |
| if (filterName != null) { |
| try { |
| setFilter((Filter) cl.loadClass(filterName).getConstructor().newInstance()); |
| } catch (Exception e) { |
| // Ignore |
| } |
| } |
| |
| // Set formatter |
| String formatterName = getProperty(className + ".formatter", null); |
| if (formatterName != null) { |
| try { |
| setFormatter((Formatter) cl.loadClass( |
| formatterName).getConstructor().newInstance()); |
| } catch (Exception e) { |
| // Ignore and fallback to defaults |
| setFormatter(new OneLineFormatter()); |
| } |
| } else { |
| setFormatter(new OneLineFormatter()); |
| } |
| |
| // Set error manager |
| setErrorManager(new ErrorManager()); |
| |
| } |
| |
| |
| private String getProperty(String name, String defaultValue) { |
| String value = LogManager.getLogManager().getProperty(name); |
| if (value == null) { |
| value = defaultValue; |
| } else { |
| value = value.trim(); |
| } |
| return value; |
| } |
| |
| |
| /** |
| * Open the new log file for the date specified by <code>date</code>. |
| */ |
| protected void open() { |
| openWriter(); |
| } |
| |
| protected void openWriter() { |
| |
| // Create the directory if necessary |
| File dir = new File(directory); |
| if (!dir.mkdirs() && !dir.isDirectory()) { |
| reportError("Unable to create [" + dir + "]", null, ErrorManager.OPEN_FAILURE); |
| writer = null; |
| return; |
| } |
| |
| // Open the current log file |
| writerLock.writeLock().lock(); |
| FileOutputStream fos = null; |
| OutputStream os = null; |
| try { |
| File pathname = new File(dir.getAbsoluteFile(), prefix |
| + (rotatable ? date : "") + suffix); |
| File parent = pathname.getParentFile(); |
| if (!parent.mkdirs() && !parent.isDirectory()) { |
| reportError("Unable to create [" + parent + "]", null, ErrorManager.OPEN_FAILURE); |
| writer = null; |
| return; |
| } |
| String encoding = getEncoding(); |
| fos = new FileOutputStream(pathname, true); |
| os = bufferSize > 0 ? new BufferedOutputStream(fos, bufferSize) : fos; |
| writer = new PrintWriter( |
| (encoding != null) ? new OutputStreamWriter(os, encoding) |
| : new OutputStreamWriter(os), false); |
| writer.write(getFormatter().getHead(this)); |
| } catch (Exception e) { |
| reportError(null, e, ErrorManager.OPEN_FAILURE); |
| writer = null; |
| if (fos != null) { |
| try { |
| fos.close(); |
| } catch (IOException e1) { |
| // Ignore |
| } |
| } |
| if (os != null) { |
| try { |
| os.close(); |
| } catch (IOException e1) { |
| // Ignore |
| } |
| } |
| } finally { |
| writerLock.writeLock().unlock(); |
| } |
| } |
| |
| private void clean() { |
| if (maxDays <= 0) { |
| return; |
| } |
| DELETE_FILES_SERVICE.submit(new Runnable() { |
| |
| @Override |
| public void run() { |
| try (DirectoryStream<Path> files = streamFilesForDelete()) { |
| for (Path file : files) { |
| Files.delete(file); |
| } |
| } catch (IOException e) { |
| reportError("Unable to delete log files older than [" + maxDays + "] days", |
| null, ErrorManager.GENERIC_FAILURE); |
| } |
| } |
| }); |
| } |
| |
| private DirectoryStream<Path> streamFilesForDelete() throws IOException { |
| final Date maxDaysOffset = getMaxDaysOffset(); |
| final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); |
| return Files.newDirectoryStream(new File(directory).toPath(), |
| new DirectoryStream.Filter<Path>() { |
| |
| @Override |
| public boolean accept(Path path) throws IOException { |
| boolean result = false; |
| String date = obtainDateFromPath(path); |
| if (date != null) { |
| try { |
| Date dateFromFile = formatter.parse(date); |
| result = dateFromFile.before(maxDaysOffset); |
| } catch (ParseException e) { |
| // no-op |
| } |
| } |
| return result; |
| } |
| }); |
| } |
| |
| private String obtainDateFromPath(Path path) { |
| Path fileName = path.getFileName(); |
| if (fileName == null) { |
| return null; |
| } |
| String date = fileName.toString(); |
| if (pattern.matcher(date).matches()) { |
| date = date.substring(prefix.length()); |
| return date.substring(0, date.length() - suffix.length()); |
| } else { |
| return null; |
| } |
| } |
| |
| private Date getMaxDaysOffset() { |
| Calendar cal = Calendar.getInstance(); |
| cal.set(Calendar.HOUR_OF_DAY, 0); |
| cal.set(Calendar.MINUTE, 0); |
| cal.set(Calendar.SECOND, 0); |
| cal.set(Calendar.MILLISECOND, 0); |
| cal.add(Calendar.DATE, -maxDays); |
| return cal.getTime(); |
| } |
| } |