You are viewing a plain text version of this content. The canonical link for it is here.
Posted to log4j-dev@logging.apache.org by ca...@apache.org on 2008/06/13 21:45:39 UTC

svn commit: r667628 - /logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java

Author: carnold
Date: Fri Jun 13 12:45:38 2008
New Revision: 667628

URL: http://svn.apache.org/viewvc?rev=667628&view=rev
Log:
Bug 45165: MultiFileAppender implementation

Added:
    logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java

Added: logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java
URL: http://svn.apache.org/viewvc/logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java?rev=667628&view=auto
==============================================================================
--- logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java (added)
+++ logging/sandbox/log4j/multifile/src/main/java/org/apache/log4j/multifile/MultiFileAppender.java Fri Jun 13 12:45:38 2008
@@ -0,0 +1,526 @@
+/*
+ * Copyright 2008 The Apache Software Foundation.
+ *
+ * Licensed 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.log4j.multifile;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.Layout;
+import org.apache.log4j.helpers.CountingQuietWriter;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.helpers.QuietWriter;
+import org.apache.log4j.spi.ErrorCode;
+import org.apache.log4j.spi.LoggingEvent;
+
+public class MultiFileAppender extends AppenderSkeleton {
+
+	/**
+	 * Do we do bufferedIO?
+	 */
+	private boolean bufferedIO = false;
+
+	/**
+	 * Determines the size of IO buffer be. Default is 8K.
+	 */
+	private int bufferSize = 8 * 1024;
+
+	/**
+	 * Immediate flush means that the underlying writer or output stream will be
+	 * flushed at the end of each append operation. Immediate flush is slower
+	 * but ensures that each append request is actually written. If
+	 * <code>immediateFlush</code> is set to <code>false</code>, then there
+	 * is a good chance that the last few logs events are not actually written
+	 * to persistent media if and when the application crashes.
+	 * 
+	 * <p>
+	 * The <code>immediateFlush</code> variable is set to <code>true</code>
+	 * by default.
+	 * 
+	 */
+	private boolean immediateFlush = true;
+
+	/**
+	 * The encoding to use when writing.
+	 * <p>
+	 * The <code>encoding</code> variable is set to <code>null</null> by
+	 * default which results in the utilization of the system's default
+	 * encoding.
+	 */
+	private String encoding;
+
+	/**
+	 * The name of the default log file. This is used if the FileNamePolicy
+	 * returns <code>null</code> for the filename.
+	 */
+	private String defaultFileName;
+
+	/**
+	 * The filename policy that determines log file name for each logging event
+	 */
+	private FileNamePolicy fileNamePolicy;
+
+	/**
+	 * 
+	 */
+	private int maxOpenFiles = 10;
+
+	/**
+	 * The time in milliseconds after which an unused file is closed - default
+	 * is 5 minutes
+	 */
+	private long idleTime = 300 * 1000;
+
+	private Date nextCleanup = new Date();
+
+	/**
+	 * Maps log filenames to the <code>SubAppenders</code> for each open file
+	 */
+	private Map openFileMap = new HashMap(maxOpenFiles * 4 / 3);
+
+	/**
+	 * list to store the <code>MultiFileAppenderListeners</code>
+	 */
+	private List listeners = new ArrayList();
+
+	/**
+	 * Default Constructor
+	 */
+	public MultiFileAppender() {
+		// does nothing
+	}
+
+	/**
+	 * 
+	 * @param layout
+	 * @param defaultFileName
+	 * @param fileNamePolicy
+	 * @param bufferedIO
+	 * @param bufferSize
+	 */
+	public MultiFileAppender(Layout layout, String defaultFileName,
+			FileNamePolicy fileNamePolicy, boolean bufferedIO, int bufferSize) {
+		this.layout = layout;
+		this.setBufferedIO(bufferedIO);
+		this.bufferSize = bufferSize;
+		this.defaultFileName = defaultFileName;
+		this.fileNamePolicy = fileNamePolicy;
+	}
+
+	/**
+	 * 
+	 * @param layout
+	 * @param defaultFileName
+	 * @param fileNamePolicy
+	 */
+	public MultiFileAppender(Layout layout, String defaultFileName,
+			FileNamePolicy fileNamePolicy) {
+		this.layout = layout;
+		this.defaultFileName = defaultFileName;
+		this.fileNamePolicy = fileNamePolicy;
+	}
+
+	public void append(LoggingEvent event) {
+		if (!checkEntryConditions()) {
+			return;
+		}
+
+		// determine log file name
+		String fileName = null;
+		if (null != fileNamePolicy) {
+			fileName = fileNamePolicy.getFileName(event);
+		}
+		if (null == fileName) {
+			if (null == defaultFileName) {
+				errorHandler
+						.error("No defaultFileName set for the appender named ["
+								+ name + "].");
+				return;
+			}
+			fileName = defaultFileName;
+		}
+
+		subAppend(fileName, event);
+
+		cleanupIdleFiles();
+	}
+
+	protected boolean checkEntryConditions() {
+		if (this.closed) {
+			LogLog.warn("Not allowed to write to a closed appender.");
+			return false;
+		}
+		if (null == this.layout) {
+			errorHandler.error("No layout set for the appender named [" + name
+					+ "].");
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * get the file and append log message
+	 * 
+	 * @param fileName
+	 * @param event
+	 */
+	protected void subAppend(String fileName, LoggingEvent event) {
+		LogFileEntry lfe;
+		// is the file already open
+		lfe = (LogFileEntry) openFileMap.get(fileName);
+		// else open/create the new file
+		if (null == lfe) {
+			lfe = openFile(fileName);
+		}
+
+		if (null != lfe) {
+			// append log event to file
+			subAppendToWriter(lfe.getQw(), event);
+
+			// fire fileAppended event
+			fireFileAppendedEvent(fileName, lfe.getQw().getCount());
+
+			// update access time
+			lfe.setLastAccess(new Date());
+		}
+	}
+
+	protected void subAppendToWriter(QuietWriter qw, LoggingEvent event) {
+		qw.write(this.layout.format(event));
+
+		if (layout.ignoresThrowable()) {
+			String[] s = event.getThrowableStrRep();
+			if (s != null) {
+				int len = s.length;
+				for (int i = 0; i < len; i++) {
+					qw.write(s[i]);
+					qw.write(Layout.LINE_SEP);
+				}
+			}
+		}
+
+		if (this.immediateFlush) {
+			qw.flush();
+		}
+	}
+
+	protected void cleanupIdleFiles() {
+		Date now = new Date();
+		// cleanup:
+		// if too many open files or time to run next check close old files
+		if (now.after(this.nextCleanup) || openFileMap.size() > maxOpenFiles) {
+			Iterator openFileIt = openFileMap.values().iterator();
+			List toDelete = new ArrayList();
+			while (openFileIt.hasNext()) {
+				LogFileEntry lfe = (LogFileEntry) openFileIt.next();
+				if (lfe.getLastAccess().getTime() + idleTime < now.getTime()) {
+					toDelete.add(lfe);
+				}
+			}
+			Iterator toDeleteIt = toDelete.iterator();
+			while (toDeleteIt.hasNext()) {
+				LogFileEntry lfe = (LogFileEntry) toDeleteIt.next();
+				openFileMap.remove(lfe.getFileName());
+				closeFile(lfe);
+			}
+			// TODO should we rather use shorter intervalls than the idleTime?
+			nextCleanup = new Date(now.getTime() + idleTime);
+		}
+		if (openFileMap.size() > maxOpenFiles) {
+			LogLog.warn("appender ["
+					+ this.name
+					+ "] has still too many open files after cleanupIdleFiles(): "
+					+ openFileMap.size() + " open files");
+		}
+	}
+
+	public synchronized void close() {
+		if (this.closed) {
+			return;
+		}
+		this.closed = true;
+		Iterator openFileIt = openFileMap.values().iterator();
+		while (openFileIt.hasNext()) {
+			closeFile((LogFileEntry) openFileIt.next());
+		}
+		openFileMap.clear();
+	}
+
+	protected LogFileEntry openFile(String fileName) {
+		FileOutputStream ostream = null;
+		try {
+			//
+			// attempt to create file
+			//
+			ostream = new FileOutputStream(fileName, true);
+		} catch (FileNotFoundException ex) {
+			//
+			// if parent directory does not exist then
+			// attempt to create it and try to create file
+			// see bug 9150
+			//
+			try {
+				String parentName = new File(fileName).getParent();
+				if (parentName != null) {
+					File parentDir = new File(parentName);
+					if (!parentDir.exists() && parentDir.mkdirs()) {
+						ostream = new FileOutputStream(fileName, true);
+					} else {
+						throw ex;
+					}
+				} else {
+					throw ex;
+				}
+			} catch (FileNotFoundException e) {
+				errorHandler.error("openFile(" + fileName + ") call failed", e,
+						ErrorCode.FILE_OPEN_FAILURE);
+				return null;
+			}
+		}
+
+		Writer writer = createWriter(ostream);
+		if (bufferedIO) {
+			writer = new BufferedWriter(writer, bufferSize);
+		}
+
+		CountingQuietWriter qw = new CountingQuietWriter(writer, errorHandler);
+
+		LogFileEntry lfe = new LogFileEntry(fileName, qw);
+		// add file into hash map
+		openFileMap.put(fileName, lfe);
+
+		writeHeader(qw);
+
+		return lfe;
+	}
+
+	/**
+	 * Write a footer as produced by the embedded layout's {@link
+	 * Layout#getFooter} method.
+	 */
+	protected void writeFooter(QuietWriter qw) {
+		if (layout != null) {
+			String f = layout.getFooter();
+			if (f != null && qw != null) {
+				qw.write(f);
+				qw.flush();
+			}
+		}
+	}
+
+	/**
+	 * Write a header as produced by the embedded layout's {@link
+	 * Layout#getHeader} method.
+	 */
+	protected void writeHeader(QuietWriter qw) {
+		if (layout != null) {
+			String h = layout.getHeader();
+			if (h != null && qw != null)
+				qw.write(h);
+		}
+	}
+
+	/**
+	 * Returns an OutputStreamWriter when passed an OutputStream. The encoding
+	 * used will depend on the value of the <code>encoding</code> property. If
+	 * the encoding value is specified incorrectly the writer will be opened
+	 * using the default system encoding (an error message will be printed to
+	 * the loglog.
+	 */
+	protected OutputStreamWriter createWriter(OutputStream os) {
+		OutputStreamWriter retval = null;
+
+		String enc = getEncoding();
+		if (enc != null) {
+			try {
+				retval = new OutputStreamWriter(os, enc);
+			} catch (IOException e) {
+				LogLog.warn("Error initializing output writer.");
+				LogLog.warn("Unsupported encoding?");
+			}
+		}
+		if (retval == null) {
+			retval = new OutputStreamWriter(os);
+		}
+		return retval;
+	}
+
+	protected void closeFile(LogFileEntry logFile) {
+		writeFooter(logFile.getQw());
+
+		try {
+			logFile.getQw().close();
+		} catch (IOException e) {
+			errorHandler.error("Could not close " + logFile.getQw(), e,
+					ErrorCode.CLOSE_FAILURE);
+		}
+	}
+
+	public boolean requiresLayout() {
+		return true;
+	}
+
+	public boolean isBufferedIO() {
+		return bufferedIO;
+	}
+
+	public void setBufferedIO(boolean bufferedIO) {
+		this.bufferedIO = bufferedIO;
+		if (bufferedIO) {
+			this.immediateFlush = false;
+		}
+	}
+
+	public int getBufferSize() {
+		return bufferSize;
+	}
+
+	public void setBufferSize(int bufferSize) {
+		this.bufferSize = bufferSize;
+	}
+
+	public String getDefaultFileName() {
+		return defaultFileName;
+	}
+
+	public void setDefaultFileName(String defaultFileName) {
+		this.defaultFileName = defaultFileName.trim();
+	}
+
+	public String getEncoding() {
+		return encoding;
+	}
+
+	public void setEncoding(String encoding) {
+		this.encoding = encoding;
+	}
+
+	public long getIdleTime() {
+		return idleTime;
+	}
+
+	public void setIdleTime(long idleTime) {
+		this.idleTime = idleTime;
+	}
+
+	public boolean isImmediateFlush() {
+		return immediateFlush;
+	}
+
+	public void setImmediateFlush(boolean immediateFlush) {
+		this.immediateFlush = immediateFlush;
+	}
+
+	public int getMaxOpenFiles() {
+		return maxOpenFiles;
+	}
+
+	public void setMaxOpenFiles(int openFileLimit) {
+		this.maxOpenFiles = openFileLimit;
+	}
+
+	public FileNamePolicy getFileNamePolicy() {
+		return fileNamePolicy;
+	}
+
+	public void setFileNamePolicy(FileNamePolicy fileNamePolicy) {
+		this.fileNamePolicy = fileNamePolicy;
+	}
+
+	public void activateOptions() {
+		// do nothing
+	}
+
+	protected void fireFileAppendedEvent(String fileName, long fileSize) {
+		Iterator listenerIt = this.listeners.iterator();
+		while (listenerIt.hasNext()) {
+			((MultiFileAppenderListener) listenerIt.next()).fileAppended(
+					fileName, fileSize);
+		}
+	}
+
+	/**
+	 * Add a MultiFileAppenderListener to this MultiFileAppender
+	 * 
+	 * @param listener
+	 */
+	public void addMultiFileAppenderListener(MultiFileAppenderListener listener) {
+		this.listeners.add(listener);
+	}
+
+	/**
+	 * Remove a MultiFileAppenderListener from this MultiFileAppender
+	 * 
+	 * @param listener
+	 */
+	public void removeMultiFileAppenderListener(
+			MultiFileAppenderListener listener) {
+		this.listeners.remove(listener);
+	}
+
+	protected static final class LogFileEntry {
+
+		private String fileName;
+
+		private CountingQuietWriter qw;
+
+		private Date lastAccess;
+
+		public LogFileEntry(String fileName, CountingQuietWriter qw) {
+			this.fileName = fileName;
+			this.qw = qw;
+			this.lastAccess = new Date();
+		}
+
+		public String getFileName() {
+			return fileName;
+		}
+
+		public void setFileName(String fileName) {
+			this.fileName = fileName;
+		}
+
+		public Date getLastAccess() {
+			return lastAccess;
+		}
+
+		public void setLastAccess(Date lastAccess) {
+			this.lastAccess = lastAccess;
+		}
+
+		public CountingQuietWriter getQw() {
+			return qw;
+		}
+
+		public void setQw(CountingQuietWriter qw) {
+			this.qw = qw;
+		}
+	}
+
+}



---------------------------------------------------------------------
To unsubscribe, e-mail: log4j-dev-unsubscribe@logging.apache.org
For additional commands, e-mail: log4j-dev-help@logging.apache.org