diff --git a/build.gradle b/build.gradle index c28e3ab0..de57c909 100644 --- a/build.gradle +++ b/build.gradle @@ -78,12 +78,13 @@ subprojects { subproject -> jackson2Version = '2.3.2' junitVersion = '4.11' log4jVersion = '1.2.17' + logbackVersion = '1.1.2' mockitoVersion = '1.9.5' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '3.3.4' springVersion = project.hasProperty('springVersion') ? project.springVersion : '4.1.0.RELEASE' - springRetryVersion = '1.1.1.RELEASE' + springRetryVersion = '1.1.2.RELEASE' } eclipse { @@ -224,6 +225,7 @@ project('spring-rabbit') { exclude group: 'com.sun.jmx', module: 'jmxri' } + compile ("ch.qos.logback:logback-classic:$logbackVersion", optional) } // suppress deprecation warnings (@SuppressWarnings("deprecation") is not enough for javac) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j/AmqpAppender.java index 3638235c..9a870a3c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j/AmqpAppender.java @@ -56,7 +56,7 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; *
* A fully-configured AmqpAppender, with every option set to their defaults, would look like this: *
- * log4j.appender.amqp=org.springframework.amqp.log4j.AmqpAppender
+ * log4j.appender.amqp=org.springframework.amqp.rabbit.log4j.AmqpAppender
* #-------------------------------
* ## Connection settings
* #-------------------------------
@@ -251,9 +251,6 @@ public class AmqpAppender extends AppenderSkeleton {
private final AtomicBoolean initializing = new AtomicBoolean();
- public AmqpAppender() {
- }
-
public String getHost() {
return host;
}
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java
new file mode 100644
index 00000000..59de5cdb
--- /dev/null
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright 2014 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 org.springframework.amqp.rabbit.logback;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+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.atomic.AtomicInteger;
+
+import org.springframework.amqp.AmqpException;
+import org.springframework.amqp.core.DirectExchange;
+import org.springframework.amqp.core.Exchange;
+import org.springframework.amqp.core.FanoutExchange;
+import org.springframework.amqp.core.HeadersExchange;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessageDeliveryMode;
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.amqp.core.TopicExchange;
+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 ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.PatternLayout;
+import ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import ch.qos.logback.core.Layout;
+
+/**
+ * A Lockback appender that publishes logging events to an AMQP Exchange.
+ *
+ * A fully-configured AmqpAppender, with every option set to their defaults, would look like this:
+ *
+ * {@code
+ *
+ *
+ * %n ]]>
+ *
+ *
+ * AmqpAppenderTest
+ * %property{applicationId}.%c.%p
+ * true
+ * UTF-8
+ * false
+ * NON_PERSISTENT
+ *
+ * }
+ *
+ *
+ * @author Artem Bilan
+ * @since 1.4
+ */
+public class AmqpAppender extends AppenderBase {
+
+ /**
+ * 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";
+
+ private final PatternLayout locationLayout = new PatternLayout();
+
+ {
+ this.locationLayout.setPattern("%nopex%class|%method|%line");
+ }
+
+ /**
+ * Logback Layout to use to generate routing key.
+ */
+ private final PatternLayout routingKeyLayout = new PatternLayout();
+
+ {
+ this.routingKeyLayout.setPattern("%nopex%c.%p");
+ }
+
+ /**
+ * Configuration arbitrary application ID.
+ */
+ private String applicationId = null;
+
+ /**
+ * Where LoggingEvents are queued to send.
+ */
+ private final LinkedBlockingQueue events = new LinkedBlockingQueue();
+
+ /**
+ * 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 final 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;
+
+ /**
+ * 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;
+
+ 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;
+
+ private Layout layout;
+
+ private TargetLengthBasedClassNameAbbreviator abbreviator;
+
+ public void setRoutingKeyPattern(String routingKeyPattern) {
+ this.routingKeyLayout.setPattern("%nopex" + routingKeyPattern);
+ }
+
+ 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 this.routingKeyLayout.getPattern();
+ }
+
+ 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 String getDeliveryMode() {
+ return this.deliveryMode.toString();
+ }
+
+ public void setDeliveryMode(String deliveryMode) {
+ this.deliveryMode = MessageDeliveryMode.valueOf(deliveryMode);
+ }
+
+ public boolean isAutoDelete() {
+ return autoDelete;
+ }
+
+ public void setAutoDelete(boolean autoDelete) {
+ this.autoDelete = autoDelete;
+ }
+
+ public boolean isGenerateId() {
+ return generateId;
+ }
+
+ public void setGenerateId(boolean generateId) {
+ this.generateId = generateId;
+ }
+
+ public String getCharset() {
+ return charset;
+ }
+
+ public void setCharset(String charset) {
+ this.charset = charset;
+ }
+
+ public void setLayout(Layout layout) {
+ this.layout = layout;
+ }
+
+ public void setAbbreviation(int len) {
+ this.abbreviator = new TargetLengthBasedClassNameAbbreviator(len);
+ }
+
+ @Override
+ public void start() {
+ super.start();
+ this.routingKeyLayout.setContext(getContext());
+ this.routingKeyLayout.start();
+ this.locationLayout.setContext(getContext());
+ this.locationLayout.start();
+ this.connectionFactory = new CachingConnectionFactory();
+ this.connectionFactory.setHost(this.host);
+ this.connectionFactory.setPort(this.port);
+ this.connectionFactory.setUsername(this.username);
+ this.connectionFactory.setPassword(this.password);
+ this.connectionFactory.setVirtualHost(this.virtualHost);
+ maybeDeclareExchange();
+ this.senderPool = Executors.newCachedThreadPool();
+ for (int i = 0; i < this.senderPoolSize; i++) {
+ this.senderPool.submit(new EventSender());
+ }
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ if (null != this.senderPool) {
+ this.senderPool.shutdownNow();
+ this.senderPool = null;
+ }
+ if (null != this.connectionFactory) {
+ this.connectionFactory.destroy();
+ }
+ this.retryTimer.cancel();
+ this.routingKeyLayout.stop();
+ }
+
+ @Override
+ protected void append(ILoggingEvent event) {
+ this.events.add(new Event(event));
+ }
+
+ /**
+ * Maybe declare the exchange.
+ */
+ protected void maybeDeclareExchange() {
+ 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);
+ }
+ admin.declareExchange(x);
+ }
+ }
+
+ /**
+ * Subclasses may modify the final message before sending.
+ * @param message The message.
+ * @param event The event.
+ * @return The modified message.
+ * @since 1.4
+ */
+ public Message postProcessMessageBeforeSend(Message message, Event event) {
+ return message;
+ }
+
+ /**
+ * Helper class to actually send LoggingEvents asynchronously.
+ */
+ protected class EventSender implements Runnable {
+ @Override
+ public void run() {
+ try {
+ RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
+ while (true) {
+ final Event event = events.take();
+ ILoggingEvent logEvent = event.getEvent();
+
+ String name = logEvent.getLoggerName();
+ Level level = logEvent.getLevel();
+
+ MessageProperties amqpProps = new MessageProperties();
+ amqpProps.setDeliveryMode(deliveryMode);
+ amqpProps.setContentType(contentType);
+ if (null != contentEncoding) {
+ amqpProps.setContentEncoding(contentEncoding);
+ }
+ amqpProps.setHeader(CATEGORY_NAME, name);
+ amqpProps.setHeader(CATEGORY_LEVEL, level.toString());
+ if (generateId) {
+ amqpProps.setMessageId(UUID.randomUUID().toString());
+ }
+
+ // Set timestamp
+ Calendar tstamp = Calendar.getInstance();
+ tstamp.setTimeInMillis(logEvent.getTimeStamp());
+ amqpProps.setTimestamp(tstamp.getTime());
+
+ // Copy properties in from MDC
+ Map props = event.getProperties();
+ Set> entrySet = props.entrySet();
+ for (Entry entry : entrySet) {
+ amqpProps.setHeader(entry.getKey(), entry.getValue());
+ }
+ String[] location = locationLayout.doLayout(logEvent).split("\\|");
+ if (!"?".equals(location[0])) {
+ amqpProps.setHeader(
+ "location",
+ String.format("%s.%s()[%s]", location[0], location[1], location[2]));
+ }
+ String msgBody;
+ String routingKey;
+ // Set applicationId, if we're using one
+ if (null != applicationId) {
+ amqpProps.setAppId(applicationId);
+ logEvent.getLoggerContextVO().getPropertyMap().put(APPLICATION_ID, applicationId);
+ }
+
+ routingKey = routingKeyLayout.doLayout(logEvent);
+
+ if (abbreviator != null && logEvent instanceof LoggingEvent) {
+ ((LoggingEvent) logEvent).setLoggerName(abbreviator.abbreviate(name));
+ msgBody = layout.doLayout(logEvent);
+ ((LoggingEvent) logEvent).setLoggerName(name);
+ }
+ else {
+ msgBody = layout.doLayout(logEvent);
+ }
+
+ // Send a message
+ try {
+ Message message = null;
+ if (AmqpAppender.this.charset != null) {
+ try {
+ message = new Message(msgBody.getBytes(AmqpAppender.this.charset), amqpProps);
+ }
+ catch (UnsupportedEncodingException e) {
+ message = new Message(msgBody.getBytes(), amqpProps);
+ }
+ }
+
+ message = postProcessMessageBeforeSend(message, event);
+ rabbitTemplate.send(exchangeName, routingKey, message);
+ }
+ 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 {
+ addError("Could not send log message " + logEvent.getMessage()
+ + " after " + maxSenderRetries + " retries", e);
+ }
+ }
+ }
+ }
+ catch (Throwable t) {
+ throw new RuntimeException(t.getMessage(), t);
+ }
+ }
+ }
+
+ /**
+ * Small helper class to encapsulate a LoggingEvent, its MDC properties, and the number of retries.
+ */
+ protected class Event {
+
+ final ILoggingEvent event;
+
+ final Map properties;
+
+ final AtomicInteger retries = new AtomicInteger(0);
+
+ public Event(ILoggingEvent event) {
+ this.event = event;
+ this.properties = this.event.getMDCPropertyMap();
+ this.event.getCallerData();
+ }
+
+ public ILoggingEvent getEvent() {
+ return event;
+ }
+
+ public Map getProperties() {
+ return properties;
+ }
+
+ public int incrementRetries() {
+ return retries.incrementAndGet();
+ }
+
+ }
+
+}
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java
new file mode 100644
index 00000000..6bb676d1
--- /dev/null
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides classes supporting Logback appenders.
+ */
+package org.springframework.amqp.rabbit.logback;
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j/AmqpAppenderIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j/AmqpAppenderIntegrationTests.java
index 6db150ee..06d9823e 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j/AmqpAppenderIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j/AmqpAppenderIntegrationTests.java
@@ -196,7 +196,7 @@ public class AmqpAppenderIntegrationTests {
listenerContainer.setMessageListener(testListener);
listenerContainer.start();
- String foo = new String("\u0fff"); // UTF-8 -> 0xe0bfbf
+ String foo = "\u0fff"; // UTF-8 -> 0xe0bfbf
log.info(foo);
assertTrue(testListener.getLatch().await(5, TimeUnit.SECONDS));
byte[] body = testListener.getMessage().getBody();
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java
new file mode 100644
index 00000000..b9c6031e
--- /dev/null
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2014 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 org.springframework.amqp.rabbit.logback;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
+import org.springframework.amqp.rabbit.log4j.AmqpAppenderConfiguration;
+import org.springframework.amqp.rabbit.log4j.TestListener;
+import org.springframework.amqp.rabbit.test.BrokerRunning;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import ch.qos.logback.classic.Logger;
+
+/**
+ * @author Artem Bilan
+ * @since 1.4
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = AmqpAppenderConfiguration.class)
+@DirtiesContext
+public class AmqpAppenderIntegrationTests {
+
+ /* logback will automatically find lockback-test.xml */
+ private final static Logger log = (Logger) LoggerFactory.getLogger(AmqpAppenderIntegrationTests.class);
+
+ @Rule
+ public BrokerRunning brokerIsRunning = BrokerRunning.isRunning();
+
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ private SimpleMessageListenerContainer listenerContainer;
+
+ @Before
+ public void setUp() throws Exception {
+ listenerContainer = applicationContext.getBean(SimpleMessageListenerContainer.class);
+ }
+
+ @After
+ public void tearDown() {
+ listenerContainer.shutdown();
+ }
+
+ @Test
+ public void testAppender() throws InterruptedException {
+ TestListener testListener = (TestListener) applicationContext.getBean("testListener", 4);
+ listenerContainer.setMessageListener(testListener);
+ listenerContainer.start();
+
+ log.debug("This is a DEBUG message");
+ log.info("This is an INFO message");
+ log.warn("This is a WARN message");
+ log.error("This is an ERROR message", new RuntimeException("Test exception"));
+
+ assertTrue(testListener.getLatch().await(5, TimeUnit.SECONDS));
+ assertNotNull(testListener.getId());
+ }
+
+ @Test
+ public void testAppenderWithProps() throws InterruptedException {
+ TestListener testListener = (TestListener) applicationContext.getBean("testListener", 4);
+ listenerContainer.setMessageListener(testListener);
+ listenerContainer.start();
+
+ String propertyName = "someproperty";
+ String propertyValue = "property.value";
+ MDC.put(propertyName, propertyValue);
+ log.debug("This is a DEBUG message with properties");
+ log.info("This is an INFO message with properties");
+ log.warn("This is a WARN message with properties");
+ log.error("This is an ERROR message with properties", new RuntimeException("Test exception"));
+ MDC.remove(propertyName);
+
+ assertTrue(testListener.getLatch().await(5, TimeUnit.SECONDS));
+ MessageProperties messageProperties = testListener.getMessageProperties();
+ assertNotNull(messageProperties);
+ assertNotNull(messageProperties.getHeaders().get(propertyName));
+ assertEquals(propertyValue, messageProperties.getHeaders().get(propertyName));
+ Object location = messageProperties.getHeaders().get("location");
+ assertNotNull(location);
+ assertThat(location, instanceOf(String.class));
+ assertThat((String) location,
+ startsWith("org.springframework.amqp.rabbit.logback.AmqpAppenderIntegrationTests.testAppenderWithProps()"));
+ }
+
+ @Test
+ public void testCharset() throws InterruptedException {
+ TestListener testListener = (TestListener) applicationContext.getBean("testListener", 1);
+ listenerContainer.setMessageListener(testListener);
+ listenerContainer.start();
+
+ String foo = "\u0fff"; // UTF-8 -> 0xe0bfbf
+ log.info(foo);
+ assertTrue(testListener.getLatch().await(5, TimeUnit.SECONDS));
+ byte[] body = testListener.getMessage().getBody();
+ int lineSeparatorExtraBytes = System.getProperty("line.separator").getBytes().length - 1;
+ assertEquals(0xe0, body[body.length - 5 - lineSeparatorExtraBytes] & 0xff);
+ assertEquals(0xbf, body[body.length - 4 - lineSeparatorExtraBytes] & 0xff);
+ assertEquals(0xbf, body[body.length - 3 - lineSeparatorExtraBytes] & 0xff);
+ }
+
+}
diff --git a/spring-rabbit/src/test/resources/logback-test.xml b/spring-rabbit/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..c640b0e1
--- /dev/null
+++ b/spring-rabbit/src/test/resources/logback-test.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ %n ]]>
+
+ 36
+ AmqpAppenderTest
+ %property{applicationId}.%c.%p
+ true
+ UTF-8
+ false
+ NON_PERSISTENT
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml
index c3c6776a..2ce2cc6b 100644
--- a/src/reference/docbook/whats-new.xml
+++ b/src/reference/docbook/whats-new.xml
@@ -66,6 +66,12 @@
Log Appender
+
+ The Logback org.springframework.amqp.rabbit.logback.AmqpAppender
+ has been introduced. It provides similar options like
+ org.springframework.amqp.rabbit.log4j.AmqpAppender .
+ For more info see JavaDocs of these classes.
+
The Log4j AmqpAppender now supports the
deliveryMode property (PERSISTENT or