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

org.apache.tomee.jul.handler.rotating.LocalFileHandler Maven / Gradle / Ivy

There is a newer version: 1.7.5
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.tomee.jul.handler.rotating;

import org.apache.commons.io.FileUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.sql.Timestamp;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
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.logging.SimpleFormatter;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * NOTE: for simplicity the prefix `org.apache.tomee.jul.handler.rotating.LocalFileHandler.` has been removed of name columns.
 * 

* |=== * | Name | Default Value | Description * | filenamePattern | ${catalina.base}/logs/logs.%s.%03d.log | where log files are created, it uses String.format() and gives you the date and file number - in this order. * | limit | 10 Megabytes | limit size indicating the file should be rotated * | dateCheckInterval | 5 seconds | how often the date should be computed to rotate the file (don't do it each time for performances reason, means you can get few records of next day in a file name with current day) * | bufferSize | -1 bytes | if positive the in memory buffer used to store data before flushing them to the disk * | encoding | - | file encoding * | level | ALL | level this handler accepts * | filter | - | filter used to check if the message should be logged * | formatter | java.util.logging.SimpleFormatter | formatter used to format messages * | archiveDirectory | ${catalina.base}/logs/archives/ | where compressed logs are put. * | archiveFormat | gzip | zip or gzip. * | archiveOlderThan | -1 days | how many days files are kept before being compressed * | purgeOlderThan | -1 days | how many days files are kept before being deleted, note: it applies on archives and not log files so 2 days of archiving and 3 days of purge makes it deleted after 5 days. * | compressionLevel | -1 | In case of zip archiving the zip compression level (-1 for off or 0-9). * |=== *

* NOTE: archiving and purging are done only when a file is rotated, it means it can be ignored during days if there is no logging activity. *

* NOTE: archiving and purging is done in a background thread pool, you can configure the number of threads in thanks to * `org.apache.tomee.jul.handler.rotating.BackgroundTaskRunner.threads` property in `conf/logging.properties`. * Default is 2 which should be fine for most applications. */ /* Open point/enhancements: - date pattern/filename pattern instead of hardcoded String.format? - write another async version? ensure it flushed well, use disruptor? -> bench seems to show it is useless */ public class LocalFileHandler extends Handler { private static final int BUFFER_SIZE = 8102; private long limit; private int bufferSize = -1; private Pattern filenameRegex; private Pattern archiveFilenameRegex; private String filenamePattern = "${catalina.base}/logs/logs.%s.%03d.log"; private String archiveFormat = "gzip"; private long dateCheckInterval = TimeUnit.SECONDS.toMillis(5); private long archiveExpiryDuration; private int compressionLevel; private long purgeExpiryDuration; private File archiveDir; private volatile int currentIndex; private volatile long lastTimestamp; private volatile String date; private volatile PrintWriter writer; private volatile int written; private final ReadWriteLock writerLock = new ReentrantReadWriteLock(); private final Lock backgroundTaskLock = new ReentrantLock(); private volatile boolean closed; public LocalFileHandler() { configure(); } private void configure() { date = currentDate(); final String className = getClass().getName(); //allow classes to override final ClassLoader cl = Thread.currentThread().getContextClassLoader(); dateCheckInterval = new Duration(getProperty(className + ".dateCheckInterval", String.valueOf(dateCheckInterval))).asMillis(); filenamePattern = replace(getProperty(className + ".filenamePattern", filenamePattern)); limit = new Size(getProperty(className + ".limit", String.valueOf("10 Mega"))).asBytes(); final int lastSep = Math.max(filenamePattern.lastIndexOf('/'), filenamePattern.lastIndexOf('\\')); String fileNameReg = lastSep >= 0 ? filenamePattern.substring(lastSep + 1) : filenamePattern; fileNameReg = fileNameReg.replace("%s", "\\d{4}\\-\\d{2}\\-\\d{2}"); // date. { // file rotation index final int indexIdxStart = fileNameReg.indexOf('%'); if (indexIdxStart >= 0) { final int indexIdxEnd = fileNameReg.indexOf('d', indexIdxStart); if (indexIdxEnd >= 0) { fileNameReg = fileNameReg.substring(0, indexIdxStart) + "\\d*" + fileNameReg.substring(indexIdxEnd + 1, fileNameReg.length()); } } } filenameRegex = Pattern.compile(fileNameReg); compressionLevel = Integer.parseInt(getProperty(className + ".compressionLevel", String.valueOf(Deflater.DEFAULT_COMPRESSION))); archiveExpiryDuration = new Duration(getProperty(className + ".archiveOlderThan", String.valueOf("-1 days"))).asMillis(); archiveDir = new File(replace(getProperty(className + ".archiveDirectory", "${catalina.base}/logs/archives/"))); archiveFormat = replace(getProperty(className + ".archiveFormat", archiveFormat)); archiveFilenameRegex = Pattern.compile(fileNameReg + "\\." + archiveFormat); purgeExpiryDuration = new Duration(getProperty(className + ".purgeOlderThan", String.valueOf("-1 days"))).asMillis(); try { bufferSize = (int) new Size(getProperty(className + ".bufferSize", "-1 b")).asBytes(); } catch (final NumberFormatException ignore) { // no-op } final String encoding = getProperty(className + ".encoding", null); if (encoding != null && encoding.length() > 0) { try { setEncoding(encoding); } catch (final UnsupportedEncodingException ex) { // no-op } } setLevel(Level.parse(getProperty(className + ".level", Level.ALL.getName()))); final String filterName = getProperty(className + ".filter", null); if (filterName != null) { try { setFilter(Filter.class.cast(cl.loadClass(filterName).newInstance())); } catch (final Exception e) { // Ignore } } final String formatterName = getProperty(className + ".formatter", null); if (formatterName != null) { try { setFormatter(Formatter.class.cast(cl.loadClass(formatterName).newInstance())); } catch (final Exception e) { setFormatter(new SimpleFormatter()); } } else { setFormatter(new SimpleFormatter()); } setErrorManager(new ErrorManager()); lastTimestamp = System.currentTimeMillis(); } protected String currentDate() { return new Timestamp(System.currentTimeMillis()).toString().substring(0, 10); } @Override public void publish(final LogRecord record) { if (!isLoggable(record)) { return; } final long now = System.currentTimeMillis(); final String tsDate; // just do it once / sec if we have a lot of log, can make some log appearing in the wrong file but better than doing it each time if (now - lastTimestamp > dateCheckInterval) { // using as much as possible volatile to avoid to lock too much lastTimestamp = now; tsDate = currentDate(); } else { tsDate = null; } try { writerLock.readLock().lock(); rotateIfNeeded(tsDate); final String result; try { result = getFormatter().format(record); } catch (final Exception e) { reportError(null, e, ErrorManager.FORMAT_FAILURE); return; } try { if (writer != null) { writer.write(result); if (bufferSize < 0) { writer.flush(); } } else { reportError(getClass().getSimpleName() + " is closed or not yet initialized, unable to log [" + result + "]", null, ErrorManager.WRITE_FAILURE); } } catch (final Exception e) { reportError(null, e, ErrorManager.WRITE_FAILURE); } } finally { writerLock.readLock().unlock(); } } private void rotateIfNeeded(final String currentDate) { if (!closed && writer == null) { try { writerLock.readLock().unlock(); writerLock.writeLock().lock(); if (!closed && writer == null) { openWriter(); } } finally { writerLock.writeLock().unlock(); writerLock.readLock().lock(); } } else if (shouldRotate(currentDate)) { try { writerLock.readLock().unlock(); writerLock.writeLock().lock(); if (shouldRotate(currentDate)) { close(); if (currentDate != null && !date.equals(currentDate)) { currentIndex = 0; date = currentDate; } openWriter(); } } finally { writerLock.writeLock().unlock(); writerLock.readLock().lock(); } } } private boolean shouldRotate(final String currentDate) { // new day, new file or limit exceeded return currentDate != null && !date.equals(currentDate) || limit > 0 && written >= limit; } @Override public void close() { closed = true; writerLock.writeLock().lock(); try { if (writer == null) { return; } writer.write(getFormatter().getTail(this)); writer.flush(); writer.close(); writer = null; } catch (final Exception e) { reportError(null, e, ErrorManager.CLOSE_FAILURE); } finally { writerLock.writeLock().unlock(); } // wait for bg tasks if running backgroundTaskLock.lock(); backgroundTaskLock.unlock(); } @Override public void flush() { writerLock.readLock().lock(); try { writer.flush(); } catch (final Exception e) { reportError(null, e, ErrorManager.FLUSH_FAILURE); } finally { writerLock.readLock().unlock(); } } protected void openWriter() { final long beforeRotation = System.currentTimeMillis(); writerLock.writeLock().lock(); FileOutputStream fos = null; OutputStream os = null; try { File pathname; do { pathname = new File(formatFilename(filenamePattern, date, currentIndex)); final File parent = pathname.getParentFile(); if (!parent.isDirectory() && !parent.mkdirs()) { reportError("Unable to create [" + parent + "]", null, ErrorManager.OPEN_FAILURE); writer = null; return; } currentIndex++; } while (pathname.isFile()); // loop to ensure we don't overwrite existing files final String encoding = getEncoding(); fos = new FileOutputStream(pathname, true); os = new CountingStream(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 (final Exception e) { reportError(null, e, ErrorManager.OPEN_FAILURE); writer = null; if (fos != null) { try { fos.close(); } catch (final IOException e1) { // no-op } } if (os != null) { try { os.close(); } catch (final IOException e1) { // no-op } } } finally { writerLock.writeLock().unlock(); } BackgroundTaskRunner.push(new Runnable() { @Override public void run() { backgroundTaskLock.lock(); try { evict(beforeRotation); } catch (final Exception e) { reportError("Can't do the log eviction", e, ErrorManager.GENERIC_FAILURE); } finally { backgroundTaskLock.unlock(); } } }); } private void evict(final long now) { if (purgeExpiryDuration > 0) { // purging archives final File[] archives = archiveDir.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return archiveFilenameRegex.matcher(name).matches(); } }); if (archives != null) { for (final File archive : archives) { try { if (FileUtils.isFileOlder(archive, now - purgeExpiryDuration)) { if (!FileUtils.deleteQuietly(archive)) { // Do not try to delete on exit cause we will find it again reportError("Failed to delete " + archive.getAbsolutePath() + ".", null, ErrorManager.GENERIC_FAILURE); } } } catch (final Exception e) { throw new IllegalStateException(e); } } } } if (archiveExpiryDuration > 0) { // archiving log files final File[] logs = new File(formatFilename(filenamePattern, "0000-00-00", 0)).getParentFile() .listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return filenameRegex.matcher(name).matches(); } }); if (logs != null) { for (final File file : logs) { try { final boolean older = FileUtils.isFileOlder(file, now); if (older && now - file.lastModified() > archiveExpiryDuration) { createArchive(file); } } catch (final Exception e) { throw new IllegalStateException(e); } } } } } private String formatFilename(final String pattern, final String date, final int index) { return String.format(pattern, date, index); } private void createArchive(final File source) { final File target = new File(archiveDir, source.getName() + "." + archiveFormat); if (target.isFile()) { return; } final File parentFile = target.getParentFile(); if (!parentFile.isDirectory() && !parentFile.mkdirs()) { throw new IllegalStateException("Can't create " + parentFile.getAbsolutePath()); } if (archiveFormat.equalsIgnoreCase("gzip")) { OutputStream outputStream = null; try { outputStream = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(target))); final byte[] buffer = new byte[BUFFER_SIZE]; FileInputStream inputStream = null; try { inputStream = new FileInputStream(source); copyStream(inputStream, outputStream, buffer); } catch (final IOException e) { throw new IllegalStateException(e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException ioe) { // no-op } } } } catch (final IOException e) { throw new IllegalStateException(e); } finally { if (outputStream != null) { try { outputStream.close(); } catch (final IOException ioe) { // no-op } } } } else { // consider file defines a zip whatever extension it is ZipOutputStream outputStream = null; try { outputStream = new ZipOutputStream(new FileOutputStream(target)); outputStream.setLevel(compressionLevel); final byte[] buffer = new byte[BUFFER_SIZE]; FileInputStream inputStream = null; try { inputStream = new FileInputStream(source); final ZipEntry zipEntry = new ZipEntry(source.getName()); outputStream.putNextEntry(zipEntry); copyStream(inputStream, outputStream, buffer); } catch (final IOException e) { throw new IllegalStateException(e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException ioe) { // no-op } } } } catch (final IOException e) { throw new IllegalStateException(e); } finally { if (outputStream != null) { try { outputStream.close(); } catch (final IOException ioe) { // no-op } } } } try { if (!FileUtils.deleteQuietly(source)) { reportError("Can't delete " + source.getAbsolutePath() + ".", null, ErrorManager.GENERIC_FAILURE); } } catch (final Exception e) { throw new IllegalStateException(e); } } private static void copyStream(final InputStream inputStream, final OutputStream outputStream, final byte[] buffer) throws IOException { int n; while ((n = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, n); } } protected String getProperty(final String name, final String defaultValue) { String value = LogManager.getLogManager().getProperty(name); if (value == null) { value = defaultValue; } else { value = value.trim(); } return value; } protected static String replace(final String str) { // [lang3] would be good but no dep for these classes is better String result = str; int start = str.indexOf("${"); if (start >= 0) { final StringBuilder builder = new StringBuilder(); int end = -1; while (start >= 0) { builder.append(str, end + 1, start); end = str.indexOf('}', start + 2); if (end < 0) { end = start - 1; break; } final String propName = str.substring(start + 2, end); String replacement = !propName.isEmpty() ? System.getProperty(propName) : null; if (replacement == null) { replacement = System.getenv(propName); } if (replacement != null) { builder.append(replacement); } else { builder.append(str, start, end + 1); } start = str.indexOf("${", end + 1); } builder.append(str, end + 1, str.length()); result = builder.toString(); } return result; } private final class CountingStream extends OutputStream { private final OutputStream out; private CountingStream(final OutputStream out) { this.out = out; written = 0; } @Override public void write(final int b) throws IOException { out.write(b); written++; } @Override public void write(final byte[] buff) throws IOException { out.write(buff); written += buff.length; } @Override public void write(final byte[] buff, final int off, final int len) throws IOException { out.write(buff, off, len); written += len; } @Override public void flush() throws IOException { out.flush(); } @Override public void close() throws IOException { out.close(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy