All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juli.FileHandler Maven / Gradle / Ivy

There is a newer version: 11.0.0
Show newest version
/*
 * 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.sql.Timestamp;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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 Handler that appends log messages to a file named {prefix}{date}{suffix} in a configured
 * directory.
 * 

* The following configuration properties are available: *

*
    *
  • directory - 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, ${catalina.base}/logs Default value: logs
  • *
  • rotatable - If true, the log file will be rotated on the first write past midnight and * the filename will be {prefix}{date}{suffix}, where date is yyyy-MM-dd. If false, the file * will not be rotated and the filename will be {prefix}{suffix}. Default value: true
  • *
  • prefix - The leading part of the log file name. Default value: juli.
  • *
  • suffix - The trailing part of the log file name. Default value: .log
  • *
  • bufferSize - Configures buffering. The value of 0 uses system default buffering * (typically an 8K buffer will be used). A value of <0 forces a writer flush upon each log write. A * value >0 uses a BufferedOutputStream with the defined value but note that the system default * buffering will also be applied. Default value: -1
  • *
  • encoding - Character set used by the log file. Default value: empty string, which means to use the * system default character set.
  • *
  • level - The level threshold for this Handler. See the java.util.logging.Level class for * the possible levels. Default value: ALL
  • *
  • filter - The java.util.logging.Filter implementation class name for this Handler. * Default value: unset
  • *
  • formatter - The java.util.logging.Formatter implementation class name for this Handler. * Default value: org.apache.juli.OneLineFormatter
  • *
  • maxDays - The maximum number of days to keep the log files. If the specified value is * <=0 then the log files will be kept on the file system forever, otherwise they will be kept the * specified maximum days. Default value: -1.
  • *
*/ public class FileHandler extends Handler { public static final int DEFAULT_MAX_DAYS = -1; public static final int DEFAULT_BUFFER_SIZE = -1; private static final ExecutorService DELETE_FILES_SERVICE = Executors .newSingleThreadExecutor(new ThreadFactory("FileHandlerLogFilesCleaner-")); // ------------------------------------------------------------ Constructor public FileHandler() { this(null, null, null); } public FileHandler(String directory, String prefix, String suffix) { this(directory, prefix, suffix, null); } public FileHandler(String directory, String prefix, String suffix, Integer maxDays) { this(directory, prefix, suffix, maxDays, null, null); } public FileHandler(String directory, String prefix, String suffix, Integer maxDays, Boolean rotatable, Integer bufferSize) { this.directory = directory; this.prefix = prefix; this.suffix = suffix; this.maxDays = maxDays; this.rotatable = rotatable; this.bufferSize = bufferSize; configure(); clean(); } // ----------------------------------------------------- Instance Variables /** * The as-of date for the currently open log file, or null if there is no open log file. */ private volatile String date = null; /** * The directory in which log files are created. */ private String directory; /** * The prefix that is added to log file filenames. */ private String prefix; /** * The suffix that is added to log file filenames. */ private String suffix; /** * Determines whether the log file is rotatable */ private Boolean rotatable; /** * Maximum number of days to keep the log files */ private Integer maxDays; /** * 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 Integer bufferSize; /** * 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 LogRecord. * * @param record description of the log event */ @Override public void publish(LogRecord record) { if (!isLoggable(record)) { return; } final String tsDate; if (rotatable.booleanValue()) { // Construct the timestamp we will use Timestamp ts = new Timestamp(System.currentTimeMillis()); tsDate = ts.toString().substring(0, 10); } else { tsDate = ""; } writerLock.readLock().lock(); try { // If the date has changed, switch log files if (!tsDate.equals(date)) { // Upgrade to writeLock before we switch writerLock.readLock().unlock(); writerLock.writeLock().lock(); try { // Make sure another thread hasn't already done this if (!tsDate.equals(date)) { 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.intValue() < 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); } } 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 = null; } 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 LogManager properties. */ private void configure() { String className = this.getClass().getName(); // allow classes to override ClassLoader cl = ClassLoaderLogManager.getClassLoader(); // Retrieve configuration of logging file name if (rotatable == null) { rotatable = Boolean.valueOf(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.booleanValue() && !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) + ")$"); if (maxDays == null) { String sMaxDays = getProperty(className + ".maxDays", String.valueOf(DEFAULT_MAX_DAYS)); try { maxDays = Integer.valueOf(sMaxDays); } catch (NumberFormatException ignore) { maxDays = Integer.valueOf(DEFAULT_MAX_DAYS); } } if (bufferSize == null) { String sBufferSize = getProperty(className + ".bufferSize", String.valueOf(DEFAULT_BUFFER_SIZE)); try { bufferSize = Integer.valueOf(sBufferSize); } catch (NumberFormatException ignore) { bufferSize = Integer.valueOf(DEFAULT_BUFFER_SIZE); } } // 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 date. */ public 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.booleanValue() ? 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.intValue() > 0 ? new BufferedOutputStream(fos, bufferSize.intValue()) : 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.intValue() <= 0 || Files.notExists(getDirectoryAsPath())) { return; } DELETE_FILES_SERVICE.submit(() -> { try (DirectoryStream 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 streamFilesForDelete() throws IOException { LocalDate maxDaysOffset = LocalDate.now().minus(maxDays.intValue(), ChronoUnit.DAYS); return Files.newDirectoryStream(getDirectoryAsPath(), path -> { boolean result = false; String date = obtainDateFromPath(path); if (date != null) { try { LocalDate dateFromFile = LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse(date)); result = dateFromFile.isBefore(maxDaysOffset); } catch (DateTimeException e) { // no-op } } return result; }); } private Path getDirectoryAsPath() { return Path.of(directory); } 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; } } protected static final class ThreadFactory implements java.util.concurrent.ThreadFactory { private final String namePrefix; private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); public ThreadFactory(final String namePrefix) { this.namePrefix = namePrefix; this.group = Thread.currentThread().getThreadGroup(); } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()); // Threads should not have as context classloader a webapp classloader t.setContextClassLoader(ThreadFactory.class.getClassLoader()); t.setDaemon(true); return t; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy