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

com.jdroid.javaweb.log.EventConsolidatingAppender Maven / Gradle / Ivy

The newest version!
package com.jdroid.javaweb.log;

import com.jdroid.java.collections.CollectionUtils;

import org.apache.log4j.Appender;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Category;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.helpers.AppenderAttachableImpl;
import org.apache.log4j.spi.AppenderAttachable;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.varia.DenyAllFilter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

/**
 * https://github.com/mattklein/event-consolidating-appender 2012-02-13
 */
public class EventConsolidatingAppender extends AppenderSkeleton implements AppenderAttachable {
	
	private final Logger logger = Logger.getLogger(this.getClass());
	
	/**
	 * Nested appenders.
	 */
	private final AppenderAttachableImpl appenders;
	private int delaySecs = 10;
	// A per-logger cache of LoggingEvents. Key is the name of the logger.
	private final ConcurrentHashMap> cachedEvents;
	// A timer for each logger for which we're caching events. Key is the name of the logger.
	private final ConcurrentHashMap timers;
	
	public EventConsolidatingAppender() {
		appenders = new AppenderAttachableImpl();
		cachedEvents = new ConcurrentHashMap<>();
		timers = new ConcurrentHashMap<>();
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#addAppender(org.apache.log4j.Appender)
	 */
	@Override
	public void addAppender(final Appender newAppender) {
		synchronized (appenders) {
			// We don't want the downstream appender to receive logging events via the normal Log4J
			// mechanism; we only want it to receive the logging events that WE send to it. So we add
			// the DenyAllFilter so that Log4J does not deliver logging events to it.
			newAppender.addFilter(new DenyAllFilter());
			appenders.addAppender(newAppender);
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#getAllAppenders()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Enumeration getAllAppenders() {
		synchronized (appenders) {
			return appenders.getAllAppenders();
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#getAppender(java.lang.String)
	 */
	@Override
	public Appender getAppender(String name) {
		synchronized (appenders) {
			return appenders.getAppender(name);
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#isAttached(org.apache.log4j.Appender)
	 */
	@Override
	public boolean isAttached(Appender appender) {
		synchronized (appenders) {
			return appenders.isAttached(appender);
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#removeAllAppenders()
	 */
	@Override
	public void removeAllAppenders() {
		synchronized (appenders) {
			appenders.removeAllAppenders();
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#removeAppender(org.apache.log4j.Appender)
	 */
	@Override
	public void removeAppender(final Appender appender) {
		synchronized (appenders) {
			appenders.removeAppender(appender);
		}
	}
	
	/**
	 * @see org.apache.log4j.spi.AppenderAttachable#removeAppender(java.lang.String)
	 */
	@Override
	public void removeAppender(String name) {
		synchronized (appenders) {
			appenders.removeAppender(name);
		}
	}
	
	public void setDelaySecs(int delaySecs) {
		this.delaySecs = delaySecs;
	}
	
	@Override
	public boolean requiresLayout() {
		return false;
	}
	
	@Override
	protected void append(LoggingEvent event) {
		
		Category logger = event.getLogger();
		
		// We synchronize on the logger, since our cache is per-logger
		synchronized (logger) {
			
			final String loggerName = logger.getName();
			
			// Append the event to the cachedEvents for this logger
			List existingEvents = cachedEvents.get(loggerName);
			if (existingEvents == null) {
				// Has to be a synchronizedList because we'll traverse and update it in another thread (the
				// timer thread)
				List newList = Collections.synchronizedList(new ArrayList());
				newList.add(event);
				cachedEvents.put(loggerName, newList);
			} else {
				existingEvents.add(event);
			}
			
			// If this is the first event we've cached for this logger, create a timer that fires after
			// the specified delay; after the delay, all of the cached events for this logger are consolidated
			// into a single event and this consolidated event is appended to the downstreamAppender.
			Timer existingTimer = timers.get(loggerName);
			if (existingTimer == null) {
				TimerTask task = new TimerTask() {
					
					@Override
					public void run() {
						consolidateEventsAndForward(loggerName);
						timers.remove(loggerName);
					}
				};
				Timer timer = new Timer();
				timer.schedule(task, delaySecs * 1000);
				timers.put(loggerName, timer);
			}
			// We've already scheduled the TimerTask for this logger; nothing to do on the else
		}
	}
	
	// Synchronized since this delivers events to the downstreamAppender -- and the downstreamAppender
	// is shared among ALL loggers
	private synchronized void consolidateEventsAndForward(String loggerName) {
		
		// TODO Add support to multiple appenders
		Appender downstreamAppender = (Appender)appenders.getAllAppenders().nextElement();
		
		List existingEvents = cachedEvents.remove(loggerName);
		if (CollectionUtils.isNotEmpty(existingEvents)) {
			LoggingEvent eventToSendDownstream;
			if (existingEvents.size() == 1) {
				eventToSendDownstream = existingEvents.get(0);
			} else {
				eventToSendDownstream = consolidatedEvent(existingEvents, downstreamAppender.getLayout());
			}
			// To deliver the consolidated event to the downstream appender, we temporarily
			// remove and then reinstate the DenyAllFilter
			downstreamAppender.clearFilters();
			downstreamAppender.doAppend(eventToSendDownstream);
			downstreamAppender.addFilter(new DenyAllFilter());
		}
	}
	
	@Override
	public void close() {
		// Not sure when or how close() will get invoked -- it doesn't seem to be invoked when the
		// process is shut down. But if it were to be invoked, this is what it should do -- clear
		// the cache of any cached events.
		// We're assuming that close() will only ever be called by one thread at a time.
		
		for (Timer timer : timers.values()) {
			timer.cancel();
		}
		timers.clear();
		
		for (String logger : cachedEvents.keySet()) {
			consolidateEventsAndForward(logger);
		}
	}
	
	/**
	 * @param existingEvents
	 * @return One new event that "consolidates" all of the passed events. On this new event:
	 *         
    *
  • Priority is the highest priority of any of the events
  • *
  • Message is a concatenation of all of the events' messages
  • *
*/ private LoggingEvent consolidatedEvent(List existingEvents, Layout layout) { Level highestLevel = null; StringBuilder consolidatedMessage = new StringBuilder(); String consolidatedFqnOfCategoryClass = null; Category consolidatedLogger = null; String linesep = System.getProperty("line.separator"); if ((existingEvents != null) && (!existingEvents.isEmpty())) { LoggingEvent firstEvent = existingEvents.get(0); highestLevel = Level.ALL; // Level.ALL is the level with the lowest rank consolidatedMessage.append(linesep); consolidatedMessage.append("The following "); consolidatedMessage.append(existingEvents.size()); consolidatedMessage.append(" events were consolidated since they occurred within "); consolidatedMessage.append(delaySecs); consolidatedMessage.append(" seconds of each other: "); consolidatedMessage.append(linesep); consolidatedFqnOfCategoryClass = firstEvent.fqnOfCategoryClass; consolidatedLogger = firstEvent.getLogger(); int eventNum = 1; for (LoggingEvent event : existingEvents) { Level thisLevel = event.getLevel(); if (thisLevel.isGreaterOrEqual(highestLevel)) { highestLevel = thisLevel; } consolidatedMessage.append(linesep + "Event " + eventNum + ":" + linesep); consolidatedMessage.append(layout.format(event)); if (layout.ignoresThrowable()) { String[] s = event.getThrowableStrRep(); if (s != null) { for (String value : s) { consolidatedMessage.append(value); consolidatedMessage.append(Layout.LINE_SEP); } } } if (!event.fqnOfCategoryClass.equals(consolidatedFqnOfCategoryClass)) { // Shouldn't be possible logger.warn("Unexpected result in logger event consolidation: category class '" + event.fqnOfCategoryClass + "' is different than expected category class '" + consolidatedFqnOfCategoryClass + "'. Using '" + event.fqnOfCategoryClass + "' as the consolidated category class."); consolidatedFqnOfCategoryClass = event.fqnOfCategoryClass; } if (event.getLogger() != consolidatedLogger) { // Shouldn't be possible logger.warn("Unexpected result in logger event consolidation: logger '" + event.getLogger() + "' is different than expected logger '" + consolidatedLogger + "'. Using '" + event.getLogger() + "' as the consolidated logger."); consolidatedLogger = event.getLogger(); } eventNum++; } } return new LoggingEvent(consolidatedFqnOfCategoryClass, consolidatedLogger, highestLevel, consolidatedMessage.toString(), null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy