vn.topmedia.monitor.MonitorAppender.java Source code

Java tutorial

Introduction

Here is the source code for vn.topmedia.monitor.MonitorAppender.java

Source

/*
 * Copyright (c) 2011 Topmedia Company
 *
 * 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 vn.topmedia.monitor;

import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.MDC;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ThrowableInformation;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.JsonMessageConverter;
import vn.topmedia.monitor.common.MonitorLevel;

/**
 * A Log4J appender that publishes logging events to an AMQP Exchange. <p> A
 * fully-configured AmqpAppender, with every option set to their defaults, would
 * look like this: </p>
 * <p/>
 * <
 * pre>
 * <code>
 *   log4j.appender.amqp=org.springframework.amqp.log4j.AmqpAppender
 *   #-------------------------------
 *   ## Connection settings
 *   #-------------------------------
 *   log4j.appender.amqp.host=localhost
 *   log4j.appender.amqp.port=5672
 *   log4j.appender.amqp.username=guest
 *   log4j.appender.amqp.password=guest
 *   log4j.appender.amqp.virtualHost=/
 *   #-------------------------------
 *   ## Exchange name and type
 *   #-------------------------------
 *   log4j.appender.amqp.exchangeName=logs
 *   log4j.appender.amqp.exchangeType=topic
 *   #-------------------------------
 *   ## Log4J-format pattern to use to create a routing key.
 *   ## The application id is available as %X{applicationId}.
 *   #-------------------------------
 *   log4j.appender.amqp.routingKeyPattern=%c.%p
 *   #-------------------------------
 *   ## Whether or not to declare this configured exchange
 *   #-------------------------------
 *   log4j.appender.amqp.declareExchange=false
 *   #-------------------------------
 *   ## Flags to use when declaring the exchange
 *   #-------------------------------
 *   log4j.appender.amqp.durable=true
 *   log4j.appender.amqp.autoDelete=false
 *   #-------------------------------
 *   ## Message properties
 *   #-------------------------------
 *   log4j.appender.amqp.contentType=text/plain
 *   log4j.appender.amqp.contentEncoding=null
 *   #-------------------------------
 *   ## Sender configuration
 *   #-------------------------------
 *   log4j.appender.amqp.senderPoolSize=2
 *   log4j.appender.amqp.maxSenderRetries=30
 *   log4j.appender.amqp.applicationId=null
 *   #-------------------------------
 *   ## Standard Log4J stuff
 *   #-------------------------------
 *   log4j.appender.amqp.layout=org.apache.log4j.PatternLayout
 *   log4j.appender.amqp.layout.ConversionPattern=%d %p %t [%c] - <%m>%n
 * </code>
 * </pre>
 *
 * @author Jon Brisbin <jbrisbin@vmware.com>
 * @author Anh Tuan <tuanta@topmedia.vn>
 */
public class MonitorAppender extends AppenderSkeleton {

    /**
     * Key name for the application id (if there is one set via the appender
     * config) in the message properties.
     */
    public static final String APPLICATION_ID = "applicationId";
    /**
     * Key name for the logger category name in the message properties
     */
    public static final String CATEGORY_NAME = "categoryName";
    /**
     * Key name for the logger level name in the message properties
     */
    public static final String CATEGORY_LEVEL = "level";
    /**
     * Name of the exchange to publish log events to.
     */
    private String exchangeName = "logs";
    /**
     * Type of the exchange to publish log events to.
     */
    private String exchangeType = "topic";
    /**
     * Log4J pattern format to use to generate a routing key.
     */
    //    private String routingKeyPattern = "%c.%p";
    /**
     * Log4J Layout to use to generate routing key.
     */
    //    private Layout routingKeyLayout = new PatternLayout(routingKeyPattern);
    /**
     * Whether or not we've tried to declare this exchange yet.
     */
    private AtomicBoolean exchangeDeclared = new AtomicBoolean(false);
    /**
     * Configuration arbitrary application ID.
     */
    private String applicationId = null;
    /**
     * Where LoggingEvents are queued to send.
     */
    private LinkedBlockingQueue<Event> events = new LinkedBlockingQueue<Event>();
    /**
     * The pool of senders.
     */
    private ExecutorService senderPool = null;
    /**
     * How many senders to use at once. Use more senders if you have lots of log
     * output going through this appender.
     */
    private int senderPoolSize = 2;
    /**
     * How many times to retry sending a message if the broker is unavailable or
     * there is some other error.
     */
    private int maxSenderRetries = 30;
    /**
     * Retries are delayed like: N ^ log(N), where N is the retry number.
     */
    private Timer retryTimer = new Timer("log-event-retry-delay", true);
    /**
     * RabbitMQ ConnectionFactory.
     */
    private AbstractConnectionFactory connectionFactory;
    /**
     * RabbitMQ host to connect to.
     */
    private String host = "localhost";
    /**
     * RabbitMQ virtual host to connect to.
     */
    private String virtualHost = "/";
    /**
     * RabbitMQ port to connect to.
     */
    private int port = 5672;
    /**
     * RabbitMQ user to connect as.
     */
    private String username = "guest";
    /**
     * RabbitMQ password for this user.
     */
    private String password = "guest";
    /**
     * Default content-type of log messages.
     */
    private String contentType = "text/plain";
    /**
     * Default content-encoding of log messages.
     */
    private String contentEncoding = null;
    /**
     * Whether or not to try and declare the configured exchange when this
     * appender starts.
     */
    private boolean declareExchange = false;
    /**
     * Used to synchronize access when creating the RabbitMQ ConnectionFactory.
     */
    private final String mutex = "mutex";
    private boolean durable = true;
    private boolean autoDelete = false;

    public MonitorAppender() {
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getVirtualHost() {
        return virtualHost;
    }

    public void setVirtualHost(String virtualHost) {
        this.virtualHost = virtualHost;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getExchangeName() {
        return exchangeName;
    }

    public void setExchangeName(String exchangeName) {
        this.exchangeName = exchangeName;
    }

    public String getExchangeType() {
        return exchangeType;
    }

    public void setExchangeType(String exchangeType) {
        this.exchangeType = exchangeType;
    }

    //    public String getRoutingKeyPattern() {
    //        return routingKeyPattern;
    //    }
    //    
    //    public void setRoutingKeyPattern(String routingKeyPattern) {
    //        this.routingKeyPattern = routingKeyPattern;
    //        this.routingKeyLayout = new PatternLayout(routingKeyPattern);
    //    }
    public boolean isDeclareExchange() {
        return declareExchange;
    }

    public void setDeclareExchange(boolean declareExchange) {
        this.declareExchange = declareExchange;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getContentEncoding() {
        return contentEncoding;
    }

    public void setContentEncoding(String contentEncoding) {
        this.contentEncoding = contentEncoding;
    }

    public String getApplicationId() {
        return applicationId;
    }

    public void setApplicationId(String applicationId) {
        this.applicationId = applicationId;
    }

    public int getSenderPoolSize() {
        return senderPoolSize;
    }

    public void setSenderPoolSize(int senderPoolSize) {
        this.senderPoolSize = senderPoolSize;
    }

    public int getMaxSenderRetries() {
        return maxSenderRetries;
    }

    public void setMaxSenderRetries(int maxSenderRetries) {
        this.maxSenderRetries = maxSenderRetries;
    }

    public boolean isDurable() {
        return durable;
    }

    public void setDurable(boolean durable) {
        this.durable = durable;
    }

    public boolean isAutoDelete() {
        return autoDelete;
    }

    public void setAutoDelete(boolean autoDelete) {
        this.autoDelete = autoDelete;
    }

    /**
     * Submit the required number of senders into the pool.
     */
    protected void startSenders() {
        senderPool = Executors.newCachedThreadPool();
        for (int i = 0; i < senderPoolSize; i++) {
            senderPool.submit(new EventSender());
        }
    }

    /**
     * Maybe declare the exchange.
     */
    protected void maybeDeclareExchange() {
        RabbitAdmin admin = new RabbitAdmin(connectionFactory);
        if (declareExchange) {
            Exchange x;
            if ("topic".equals(exchangeType)) {
                x = new TopicExchange(exchangeName, durable, autoDelete);
            } else if ("direct".equals(exchangeType)) {
                x = new DirectExchange(exchangeName, durable, autoDelete);
            } else if ("fanout".equals(exchangeType)) {
                x = new FanoutExchange(exchangeName, durable, autoDelete);
            } else if ("headers".equals(exchangeType)) {
                x = new HeadersExchange(exchangeType, durable, autoDelete);
            } else {
                x = new TopicExchange(exchangeName, durable, autoDelete);
            }
            admin.declareExchange(x);
        }
    }

    @Override
    public void append(LoggingEvent event) {
        if (null == senderPool) {
            synchronized (mutex) {
                connectionFactory = new CachingConnectionFactory();
                connectionFactory.setHost(host);
                connectionFactory.setPort(port);
                connectionFactory.setUsername(username);
                connectionFactory.setPassword(password);
                connectionFactory.setVirtualHost(virtualHost);
                maybeDeclareExchange();
                exchangeDeclared.set(true);

                startSenders();
            }
        }
        events.add(new Event(event, event.getProperties()));
    }

    public void close() {
        if (null != senderPool) {
            senderPool.shutdownNow();
            senderPool = null;
        }
        if (null != connectionFactory) {
            connectionFactory.destroy();
        }
    }

    public boolean requiresLayout() {
        return true;
    }

    /**
     * Helper class to actually send LoggingEvents asynchronously.
     */
    protected class EventSender implements Runnable {

        public void run() {
            try {
                RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
                rabbitTemplate.setMessageConverter(new JsonMessageConverter());
                rabbitTemplate.setExchange(exchangeName);
                while (true) {
                    final Event event = events.take();
                    LoggingEvent logEvent = event.getEvent();
                    try {
                        rabbitTemplate.convertAndSend(convertEvent(event));
                    } catch (AmqpException e) {
                        int retries = event.incrementRetries();
                        if (retries < maxSenderRetries) {
                            // Schedule a retry based on the number of times I've tried to re-send this
                            retryTimer.schedule(new TimerTask() {

                                @Override
                                public void run() {
                                    events.add(event);
                                }
                            }, (long) (Math.pow(retries, Math.log(retries)) * 1000));
                        } else {
                            errorHandler.error("Could not send log message " + logEvent.getRenderedMessage()
                                    + " after " + maxSenderRetries + " retries", e, ErrorCode.WRITE_FAILURE,
                                    logEvent);
                        }
                    } finally {
                        if (null != applicationId) {
                            MDC.remove(APPLICATION_ID);
                        }
                    }
                }
            } catch (Throwable t) {
                throw new RuntimeException(t.getMessage(), t);
            }
        }

        /**
         * Convert Event to MonitorBean.
         *
         * @param event Event logging.
         * @return MonitorBean.
         */
        private MonitorBean convertEvent(Event event) {
            LoggingEvent logEvent = event.getEvent();
            StringBuilder msgBody = new StringBuilder(String.format("%s%n", logEvent.getRenderedMessage()));
            if (null != logEvent.getThrowableInformation()) {
                ThrowableInformation tinfo = logEvent.getThrowableInformation();
                for (String line : tinfo.getThrowableStrRep()) {
                    msgBody.append(String.format("%s%n", line));
                }
            }
            MonitorBean bean = new MonitorBean();
            bean.setClazz(logEvent.getLoggerName());
            bean.setServiceId("Logger");
            Level level = logEvent.getLevel();
            if (level.equals(Level.INFO)) {
                bean.setLevel(MonitorLevel.INFO);
            } else if (level.equals(Level.ERROR)) {
                bean.setLevel(MonitorLevel.ERROR);
            } else {
                bean.setLevel(MonitorLevel.DEBUG);
            }
            bean.setType(applicationId);
            bean.setInfo(msgBody.toString());
            return bean;
        }
    }

    /**
     * Small helper class to encapsulate a LoggingEvent, its MDC properties, and
     * the number of retries.
     */
    @SuppressWarnings("rawtypes")
    protected class Event {

        final LoggingEvent event;
        final Map properties;
        AtomicInteger retries = new AtomicInteger(0);

        public Event(LoggingEvent event, Map properties) {
            this.event = event;
            this.properties = properties;
        }

        public LoggingEvent getEvent() {
            return event;
        }

        public Map getProperties() {
            return properties;
        }

        public int incrementRetries() {
            return retries.incrementAndGet();
        }
    }
}