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

io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.log4j2.AmqpAppender Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016 the original author or authors.
 *
 * 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 io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.log4j2;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import io.bitsensor.plugins.shaded.org.apache.logging.log4j.Level;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.Filter;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.Layout;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.LogEvent;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.LoggerContext;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.appender.AbstractAppender;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.appender.AbstractManager;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.Configuration;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.plugins.Plugin;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.plugins.PluginElement;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.config.plugins.PluginFactory;
import io.bitsensor.plugins.shaded.org.apache.logging.log4j.core.layout.PatternLayout;

import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.AmqpException;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.DirectExchange;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.Exchange;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.FanoutExchange;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.HeadersExchange;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.Message;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.MessageDeliveryMode;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.MessageProperties;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.core.TopicExchange;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection.AbstractConnectionFactory;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.core.DeclareExchangeConnectionListener;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.core.RabbitAdmin;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.core.RabbitTemplate;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.support.LogAppenderUtils;
import io.bitsensor.plugins.shaded.org.springframework.retry.RetryPolicy;
import io.bitsensor.plugins.shaded.org.springframework.retry.policy.SimpleRetryPolicy;
import io.bitsensor.plugins.shaded.org.springframework.retry.support.RetryTemplate;

import io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory;

/**
 * A Log4j 2 appender that publishes logging events to an AMQP Exchange.
 *
 * @author Gary Russell
 * @author Stephen Oakey
 * @author Artem Bilan
 *
 * @since 1.6
 */
@Plugin(name = "RabbitMQ", category = "Core", elementType = "appender", printObject = true)
public class AmqpAppender extends AbstractAppender {

	/**
	 * 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";

	/**
	 * Tha manager.
	 */
	private final AmqpManager manager;

	/**
	 * The template.
	 */
	private final RabbitTemplate rabbitTemplate = new RabbitTemplate();

	/**
	 * Where LoggingEvents are queued to send.
	 */
	private final LinkedBlockingQueue events = new LinkedBlockingQueue();

	/**
	 * Used to synchronize access to pattern layouts.
	 */
	private final Object layoutMutex = new Object();


	public AmqpAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions,
			AmqpManager manager) {
		super(name, filter, layout, ignoreExceptions);
		this.manager = manager;
	}

	@PluginFactory
	public static AmqpAppender createAppender(
			@PluginConfiguration final Configuration configuration,
			@PluginAttribute("name") String name,
			@PluginElement("Layout") Layout layout,
			@PluginElement("Filter") Filter filter,
			@PluginAttribute("ignoreExceptions") boolean ignoreExceptions,
			@PluginAttribute("host") String host,
			@PluginAttribute("port") int port,
			@PluginAttribute("addresses") String addresses,
			@PluginAttribute("user") String user,
			@PluginAttribute("password") String password,
			@PluginAttribute("virtualHost") String virtualHost,
			@PluginAttribute("senderPoolSize") int senderPoolSize,
			@PluginAttribute("maxSenderRetries") int maxSenderRetries,
			@PluginAttribute("applicationId") String applicationId,
			@PluginAttribute("routingKeyPattern") String routingKeyPattern,
			@PluginAttribute("generateId") boolean generateId,
			@PluginAttribute("deliveryMode") String deliveryMode,
			@PluginAttribute("exchange") String exchange,
			@PluginAttribute("exchangeType") String exchangeType,
			@PluginAttribute("declareExchange") boolean declareExchange,
			@PluginAttribute("durable") boolean durable,
			@PluginAttribute("autoDelete") boolean autoDelete,
			@PluginAttribute("contentType") String contentType,
			@PluginAttribute("contentEncoding") String contentEncoding,
			@PluginAttribute("clientConnectionProperties") String clientConnectionProperties,
			@PluginAttribute("async") boolean async,
			@PluginAttribute("charset") String charset) {
		if (name == null) {
			LOGGER.error("No name for AmqpAppender");
		}
		Layout theLayout = layout;
		if (theLayout == null) {
			theLayout = PatternLayout.createDefaultLayout();
		}
		AmqpManager manager = new AmqpManager(configuration.getLoggerContext(), name);
		manager.host = host;
		manager.port = port;
		manager.addresses = addresses;
		manager.username = user;
		manager.password = password;
		manager.virtualHost = virtualHost;
		manager.senderPoolSize = senderPoolSize;
		manager.maxSenderRetries = maxSenderRetries;
		manager.applicationId = applicationId;
		manager.routingKeyPattern = routingKeyPattern;
		manager.generateId = generateId;
		manager.deliveryMode = MessageDeliveryMode.valueOf(deliveryMode);
		manager.exchangeName = exchange;
		manager.exchangeType = exchangeType;
		manager.declareExchange = declareExchange;
		manager.durable = durable;
		manager.autoDelete = autoDelete;
		manager.contentType = contentType;
		manager.contentEncoding = contentEncoding;
		manager.clientConnectionProperties = clientConnectionProperties;
		manager.charset = charset;
		manager.async = async;
		AmqpAppender appender = new AmqpAppender(name, filter, theLayout, ignoreExceptions, manager);
		if (manager.activateOptions()) {
			appender.startSenders();
			return appender;
		}
		return null;
	}

	/**
	 * Submit the required number of senders into the pool.
	 */
	private void startSenders() {
		this.rabbitTemplate.setConnectionFactory(this.manager.connectionFactory);
		if (this.manager.async) {
			for (int i = 0; i < this.manager.senderPoolSize; i++) {
				this.manager.senderPool.submit(new EventSender());
			}
		}
		else if (this.manager.maxSenderRetries > 0) {
			RetryTemplate retryTemplate = new RetryTemplate();
			RetryPolicy retryPolicy = new SimpleRetryPolicy(this.manager.maxSenderRetries);
			retryTemplate.setRetryPolicy(retryPolicy);
			this.rabbitTemplate.setRetryTemplate(retryTemplate);
		}
	}


	@Override
	public void append(LogEvent event) {
		Event appenderEvent = new Event(event, event.getContextData().toMap());
		if (this.manager.async) {
			this.events.add(appenderEvent);
		}
		else {
			sendEvent(appenderEvent, appenderEvent.getProperties());
		}
	}

	/**
	 * Subclasses may modify the final message before sending.
	 * @param message The message.
	 * @param event The event.
	 * @return The modified message.
	 */
	public Message postProcessMessageBeforeSend(Message message, Event event) {
		return message;
	}

	private void sendEvent(final Event event, Map properties) {
		LogEvent logEvent = event.getEvent();
		String name = logEvent.getLoggerName();
		Level level = logEvent.getLevel();

		MessageProperties amqpProps = new MessageProperties();
		amqpProps.setDeliveryMode(this.manager.deliveryMode);
		amqpProps.setContentType(this.manager.contentType);
		if (null != this.manager.contentEncoding) {
			amqpProps.setContentEncoding(this.manager.contentEncoding);
		}
		amqpProps.setHeader(CATEGORY_NAME, name);
		amqpProps.setHeader(CATEGORY_LEVEL, level.toString());
		if (this.manager.generateId) {
			amqpProps.setMessageId(UUID.randomUUID().toString());
		}

		// Set applicationId, if we're using one
		if (null != this.manager.applicationId) {
			amqpProps.setAppId(this.manager.applicationId);
		}

		// Set timestamp
		Calendar tstamp = Calendar.getInstance();
		tstamp.setTimeInMillis(logEvent.getTimeMillis());
		amqpProps.setTimestamp(tstamp.getTime());

		// Copy properties in from MDC
		for (Entry entry : properties.entrySet()) {
			amqpProps.setHeader(entry.getKey().toString(), entry.getValue());
		}
		if (logEvent.getSource() != null) {
			amqpProps.setHeader(
					"location",
					String.format("%s.%s()[%s]", logEvent.getSource().getClassName(),
							logEvent.getSource().getMethodName(),
							logEvent.getSource().getLineNumber()));
		}

		StringBuilder msgBody;
		String routingKey;

		// Send a message
		try {
			synchronized (this.layoutMutex) {
				msgBody = new StringBuilder(new String(getLayout().toByteArray(logEvent), "UTF-8"));
				routingKey = new String(this.manager.routingKeyLayout.toByteArray(logEvent),
						"UTF-8");
			}
			Message message = null;
			if (this.manager.charset != null) {
				try {
					message = new Message(msgBody.toString().getBytes(this.manager.charset),
							amqpProps);
				}
				catch (UnsupportedEncodingException e) { /* fall back to default */ }
			}
			if (message == null) {
				message = new Message(msgBody.toString().getBytes(), amqpProps); //NOSONAR (default charset)
			}
			message = postProcessMessageBeforeSend(message, event);
			this.rabbitTemplate.send(this.manager.exchangeName, routingKey, message);
		}
		catch (AmqpException e) {
			int retries = event.incrementRetries();
			if (this.manager.async && retries < this.manager.maxSenderRetries) {
				// Schedule a retry based on the number of times I've tried to re-send this
				this.manager.retryTimer.schedule(new TimerTask() {

					@Override
					public void run() {
						AmqpAppender.this.events.add(event);
					}

				}, (long) (Math.pow(retries, Math.log(retries)) * 1000));
			}
			else {
				getHandler().error("Could not send log message " + logEvent.getMessage()
						+ " after " + this.manager.maxSenderRetries + " retries", logEvent, e);
			}
		}
		catch (Exception e) {
			getHandler().error("Could not send log message " + logEvent.getMessage(), logEvent, e);
		}
	}

	@Override
	protected boolean stop(long timeout, TimeUnit timeUnit, boolean changeLifeCycleState) {
		boolean stopped = super.stop(timeout, timeUnit, changeLifeCycleState);
		return stopped & this.manager.stop(timeout, timeUnit);
	}

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

		@Override
		public void run() {
			try {
				// We leave the loop when thread is interrupted by the ExecutorService.shutdownNow()
				while (true) {
					final Event event = AmqpAppender.this.events.take();
					sendEvent(event, event.getProperties());
				}
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
	}

	/**
	 * Helper class to encapsulate a LoggingEvent, its MDC properties, and the number of retries.
	 */
	protected static class Event {

		private final LogEvent event;

		private final Map properties;

		private final AtomicInteger retries = new AtomicInteger(0);

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

		public LogEvent getEvent() {
			return this.event;
		}

		public Map getProperties() {
			return this.properties;
		}

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

	}

	protected static class AmqpManager extends AbstractManager {

		/**
		 * True to send events on separate threads.
		 */
		private boolean async;

		/**
		 * 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;

		/**
		 * Configuration arbitrary application ID.
		 */
		private String applicationId = 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;

		/**
		 * RabbitMQ ConnectionFactory.
		 */
		private AbstractConnectionFactory connectionFactory;

		/**
		 * RabbitMQ host to connect to.
		 */
		private String host = "localhost";

		/**
		 * A comma-delimited list of broker addresses: host:port[,host:port]*.
		 */
		private String addresses;

		/**
		 * 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;

		/**
		 * Additional client connection properties to be added to the rabbit connection,
		 * with the form {@code key:value[,key:value]...}.
		 */
		private String clientConnectionProperties;

		/**
		 * charset to use when converting String to byte[], default null (system default charset used).
		 * If the charset is unsupported on the current platform, we fall back to using
		 * the system charset.
		 */
		private String charset = Charset.defaultCharset().name();

		private boolean durable = true;

		private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT;

		private boolean autoDelete = false;

		/**
		 * Used to determine whether {@link MessageProperties#setMessageId(String)} is set.
		 */
		private boolean generateId = false;

		/**
		 * The pool of senders.
		 */
		private ExecutorService senderPool = null;

		/**
		 * Retries are delayed like: N ^ log(N), where N is the retry number.
		 */
		private final Timer retryTimer = new Timer("log-event-retry-delay", true);

		protected AmqpManager(LoggerContext loggerContext, String name) {
			super(loggerContext, name);
		}

		private boolean activateOptions() {
			ConnectionFactory rabbitConnectionFactory = createRabbitConnectionFactory();
			if (rabbitConnectionFactory != null) {
				this.routingKeyLayout = PatternLayout.createLayout(this.routingKeyPattern
								.replaceAll("%X\\{applicationId\\}", this.applicationId),
						null, null, null, Charset.forName(this.charset), false, true, null, null);
				this.connectionFactory = new CachingConnectionFactory(createRabbitConnectionFactory());
				if (this.addresses != null) {
					this.connectionFactory.setAddresses(this.addresses);
				}
				if (this.clientConnectionProperties != null) {
					LogAppenderUtils.updateClientConnectionProperties(this.connectionFactory,
							this.clientConnectionProperties);
				}
				setUpExchangeDeclaration();
				this.senderPool = Executors.newCachedThreadPool();
				return true;
			}
			return false;
		}

		/**
		 * Create the {@link ConnectionFactory}.
		 *
		 * @return a {@link ConnectionFactory}.
		 */
		protected ConnectionFactory createRabbitConnectionFactory() {
			RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean();
			configureRabbitConnectionFactory(factoryBean);
			try {
				factoryBean.afterPropertiesSet();
				return factoryBean.getObject();
			}
			catch (Exception e) {
				LOGGER.error("Failed to create customized Rabbit ConnectionFactory.", e);
				return null;
			}
		}

		/**
		 * Configure the {@link RabbitConnectionFactoryBean}. Sub-classes may override to
		 * customize the configuration of the bean.
		 *
		 * @param factoryBean the {@link RabbitConnectionFactoryBean}.
		 */
		protected void configureRabbitConnectionFactory(RabbitConnectionFactoryBean factoryBean) {
			factoryBean.setHost(this.host);
			factoryBean.setPort(this.port);
			factoryBean.setUsername(this.username);
			factoryBean.setPassword(this.password);
			factoryBean.setVirtualHost(this.virtualHost);
		}

		@Override
		protected boolean releaseSub(long timeout, TimeUnit timeUnit) {
			this.retryTimer.cancel();
			this.senderPool.shutdownNow();
			this.connectionFactory.destroy();
			try {
				return this.senderPool.awaitTermination(timeout, timeUnit);
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				throw new IllegalStateException(e);
			}
		}

		protected void setUpExchangeDeclaration() {
			RabbitAdmin admin = new RabbitAdmin(this.connectionFactory);
			if (this.declareExchange) {
				Exchange x;
				if ("topic".equals(this.exchangeType)) {
					x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete);
				}
				else if ("direct".equals(this.exchangeType)) {
					x = new DirectExchange(this.exchangeName, this.durable, this.autoDelete);
				}
				else if ("fanout".equals(this.exchangeType)) {
					x = new FanoutExchange(this.exchangeName, this.durable, this.autoDelete);
				}
				else if ("headers".equals(this.exchangeType)) {
					x = new HeadersExchange(this.exchangeType, this.durable, this.autoDelete);
				}
				else {
					x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete);
				}
				this.connectionFactory.addConnectionListener(new DeclareExchangeConnectionListener(x, admin));
			}
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy