From abc96cf829cabe48b21c79c3987742ebf30c135a Mon Sep 17 00:00:00 2001 From: Francesco Scipioni <24493497+Scip88@users.noreply.github.com> Date: Tue, 21 Jan 2020 15:14:35 +0100 Subject: [PATCH] Use of PluginBuilderFactory on AmqpAppender (Log4j2) (#1144) * resolve #1142 Refactor @PluginFactory and use of @PluginBuilderFactory Simplify subclasses implementation of AmqpAppender (Log4j2) * Change Reference Documentation Add Example for Log4j2 subclasses and static factor * add test and fix logging.adoc * fix LICENSE and logging.adoc * fix missing rename file * ConnectionFactory destroy() to avoid resources leaking --- .../amqp/rabbit/log4j2/AmqpAppender.java | 543 ++++++++++++++++-- .../amqp/rabbit/log4j2/AmqpAppenderTests.java | 1 + .../rabbit/log4j2/ExtendAmqpAppender.java | 85 +++ .../log4j2/ExtendAmqpAppenderTests.java | 253 ++++++++ .../resources/log4j2-extend-amqp-appender.xml | 56 ++ src/reference/asciidoc/logging.adoc | 41 +- 6 files changed, 917 insertions(+), 62 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppender.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java create mode 100644 spring-rabbit/src/test/resources/log4j2-extend-amqp-appender.xml diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java index 812a14f138..0904f2fe7a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -45,6 +45,8 @@ import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; @@ -89,6 +91,7 @@ * @author Dominique Villard * @author Nicolas Ristock * @author Eugene Gusev + * @author Francesco Scipioni * * @since 1.6 */ @@ -144,6 +147,7 @@ public AmqpAppender(String name, Filter filter, Layout l this.events = eventQueue; } + @Deprecated // For backward compatibility @PluginFactory public static AmqpAppender createAppender(// NOSONAR NCSS line count @PluginConfiguration final Configuration configuration, @@ -190,66 +194,56 @@ public static AmqpAppender createAppender(// NOSONAR NCSS line count @PluginElement(BlockingQueueFactory.ELEMENT_TYPE) BlockingQueueFactory blockingQueueFactory, @PluginAttribute(value = "addMdcAsHeaders", defaultBoolean = true) boolean addMdcAsHeaders) { - 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); - JavaUtils.INSTANCE - .acceptIfNotNull(uri, value -> manager.uri = value) - .acceptIfNotNull(host, value -> manager.host = value) - .acceptIfNotNull(port, value -> manager.port = Integers.parseInt(value)) - .acceptIfNotNull(addresses, value -> manager.addresses = value) - .acceptIfNotNull(user, value -> manager.username = value) - .acceptIfNotNull(password, value -> manager.password = value) - .acceptIfNotNull(virtualHost, value -> manager.virtualHost = value) - .acceptIfNotNull(useSsl, value -> manager.useSsl = value) - .acceptIfNotNull(verifyHostname, value -> manager.verifyHostname = value) - .acceptIfNotNull(sslAlgorithm, value -> manager.sslAlgorithm = value) - .acceptIfNotNull(sslPropertiesLocation, value -> manager.sslPropertiesLocation = value) - .acceptIfNotNull(keyStore, value -> manager.keyStore = value) - .acceptIfNotNull(keyStorePassphrase, value -> manager.keyStorePassphrase = value) - .acceptIfNotNull(keyStoreType, value -> manager.keyStoreType = value) - .acceptIfNotNull(trustStore, value -> manager.trustStore = value) - .acceptIfNotNull(trustStorePassphrase, value -> manager.trustStorePassphrase = value) - .acceptIfNotNull(trustStoreType, value -> manager.trustStoreType = value) - .acceptIfNotNull(saslConfig, value -> manager.saslConfig = value) - .acceptIfNotNull(senderPoolSize, value -> manager.senderPoolSize = value) - .acceptIfNotNull(maxSenderRetries, value -> manager.maxSenderRetries = value) - .acceptIfNotNull(applicationId, value -> manager.applicationId = value) - .acceptIfNotNull(routingKeyPattern, value -> manager.routingKeyPattern = value) - .acceptIfNotNull(generateId, value -> manager.generateId = value) - .acceptIfNotNull(deliveryMode, value -> manager.deliveryMode = MessageDeliveryMode.valueOf(deliveryMode)) - .acceptIfNotNull(exchange, value -> manager.exchangeName = value) - .acceptIfNotNull(exchangeType, value -> manager.exchangeType = value) - .acceptIfNotNull(declareExchange, value -> manager.declareExchange = value) - .acceptIfNotNull(durable, value -> manager.durable = value) - .acceptIfNotNull(autoDelete, value -> manager.autoDelete = value) - .acceptIfNotNull(contentType, value -> manager.contentType = value) - .acceptIfNotNull(contentEncoding, value -> manager.contentEncoding = value) - .acceptIfNotNull(connectionName, value -> manager.connectionName = value) - .acceptIfNotNull(clientConnectionProperties, value -> manager.clientConnectionProperties = value) - .acceptIfNotNull(charset, value -> manager.charset = value) - .acceptIfNotNull(async, value -> manager.async = value) - .acceptIfNotNull(addMdcAsHeaders, value -> manager.addMdcAsHeaders = value); - - BlockingQueue eventQueue; - if (blockingQueueFactory == null) { - eventQueue = new LinkedBlockingQueue<>(bufferSize); - } - else { - eventQueue = blockingQueueFactory.create(bufferSize); - } + return new Builder() + .setConfiguration(configuration) + .setName(name) + .setLayout(layout) + .setFilter(filter) + .setIgnoreExceptions(ignoreExceptions) + .setUri(uri) + .setHost(host) + .setPort(port) + .setAddresses(addresses) + .setUser(user) + .setPassword(password) + .setVirtualHost(virtualHost) + .setUseSsl(useSsl) + .setVerifyHostname(verifyHostname) + .setSslAlgorithm(sslAlgorithm) + .setSslPropertiesLocation(sslPropertiesLocation) + .setKeyStore(keyStore) + .setKeyStorePassphrase(keyStorePassphrase) + .setKeyStoreType(keyStoreType) + .setTrustStore(trustStore) + .setTrustStorePassphrase(trustStorePassphrase) + .setTrustStoreType(trustStoreType) + .setSaslConfig(saslConfig) + .setSenderPoolSize(senderPoolSize) + .setMaxSenderRetries(maxSenderRetries) + .setApplicationId(applicationId) + .setRoutingKeyPattern(routingKeyPattern) + .setGenerateId(generateId) + .setDeliveryMode(deliveryMode) + .setExchange(exchange) + .setExchangeType(exchangeType) + .setDeclareExchange(declareExchange) + .setDurable(durable) + .setAutoDelete(autoDelete) + .setContentType(contentType) + .setContentEncoding(contentEncoding) + .setConnectionName(connectionName) + .setClientConnectionProperties(clientConnectionProperties) + .setAsync(async) + .setCharset(charset) + .setBufferSize(bufferSize) + .setBlockingQueueFactory(blockingQueueFactory) + .setAddMdcAsHeaders(addMdcAsHeaders) + .build(); + } - AmqpAppender appender = new AmqpAppender(name, filter, theLayout, ignoreExceptions, manager, eventQueue); - if (manager.activateOptions()) { - appender.startSenders(); - return appender; - } - return null; + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); } /** @@ -785,4 +779,433 @@ else if ("headers".equals(this.exchangeType)) { } + protected static class Builder implements org.apache.logging.log4j.core.util.Builder { + + @PluginConfiguration + private Configuration configuration; + + @PluginBuilderAttribute("name") + private String name; + + @PluginElement("Layout") + private Layout layout; + + @PluginElement("Filter") + private Filter filter; + + @PluginBuilderAttribute("ignoreExceptions") + private boolean ignoreExceptions; + + @PluginBuilderAttribute("uri") + private URI uri; + + @PluginBuilderAttribute("host") + private String host; + + @PluginBuilderAttribute("port") + private String port; + + @PluginBuilderAttribute("addresses") + private String addresses; + + @PluginBuilderAttribute("user") + private String user; + + @PluginBuilderAttribute("password") + private String password; + + @PluginBuilderAttribute("virtualHost") + private String virtualHost; + + @PluginBuilderAttribute("useSsl") + private boolean useSsl; + + @PluginBuilderAttribute("verifyHostname") + private boolean verifyHostname; + + @PluginBuilderAttribute("sslAlgorithm") + private String sslAlgorithm; + + @PluginBuilderAttribute("sslPropertiesLocation") + private String sslPropertiesLocation; + + @PluginBuilderAttribute("keyStore") + private String keyStore; + + @PluginBuilderAttribute("keyStorePassphrase") + private String keyStorePassphrase; + + @PluginBuilderAttribute("keyStoreType") + private String keyStoreType; + + @PluginBuilderAttribute("trustStore") + private String trustStore; + + @PluginBuilderAttribute("trustStorePassphrase") + private String trustStorePassphrase; + + @PluginBuilderAttribute("trustStoreType") + private String trustStoreType; + + @PluginBuilderAttribute("saslConfig") + private String saslConfig; + + @PluginBuilderAttribute("senderPoolSize") + private int senderPoolSize; + + @PluginBuilderAttribute("maxSenderRetries") + private int maxSenderRetries; + + @PluginBuilderAttribute("applicationId") + private String applicationId; + + @PluginBuilderAttribute("routingKeyPattern") + private String routingKeyPattern; + + @PluginBuilderAttribute("generateId") + private boolean generateId; + + @PluginBuilderAttribute("deliveryMode") + private String deliveryMode; + + @PluginBuilderAttribute("exchange") + private String exchange; + + @PluginBuilderAttribute("exchangeType") + private String exchangeType; + + @PluginBuilderAttribute("declareExchange") + private boolean declareExchange; + + @PluginBuilderAttribute("durable") + private boolean durable; + + @PluginBuilderAttribute("autoDelete") + private boolean autoDelete; + + @PluginBuilderAttribute("contentType") + private String contentType; + + @PluginBuilderAttribute("contentEncoding") + private String contentEncoding; + + @PluginBuilderAttribute("connectionName") + private String connectionName; + + @PluginBuilderAttribute("clientConnectionProperties") + private String clientConnectionProperties; + + @PluginBuilderAttribute("async") + private boolean async; + + @PluginBuilderAttribute("charset") + private String charset; + + @PluginBuilderAttribute("bufferSize") + private int bufferSize = Integer.MAX_VALUE; + + @PluginElement(BlockingQueueFactory.ELEMENT_TYPE) + private BlockingQueueFactory blockingQueueFactory; + + @PluginBuilderAttribute("addMdcAsHeaders") + private boolean addMdcAsHeaders = Boolean.TRUE; + + public Builder setConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + public Builder setName(String name) { + this.name = name; + return this; + } + + public Builder setLayout(Layout layout) { + this.layout = layout; + return this; + } + + public Builder setFilter(Filter filter) { + this.filter = filter; + return this; + } + + public Builder setIgnoreExceptions(boolean ignoreExceptions) { + this.ignoreExceptions = ignoreExceptions; + return this; + } + + public Builder setUri(URI uri) { + this.uri = uri; + return this; + } + + public Builder setHost(String host) { + this.host = host; + return this; + } + + public Builder setPort(String port) { + this.port = port; + return this; + } + + public Builder setAddresses(String addresses) { + this.addresses = addresses; + return this; + } + + public Builder setUser(String user) { + this.user = user; + return this; + } + + public Builder setPassword(String password) { + this.password = password; + return this; + } + + public Builder setVirtualHost(String virtualHost) { + this.virtualHost = virtualHost; + return this; + } + + public Builder setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + return this; + } + + public Builder setVerifyHostname(boolean verifyHostname) { + this.verifyHostname = verifyHostname; + return this; + } + + public Builder setSslAlgorithm(String sslAlgorithm) { + this.sslAlgorithm = sslAlgorithm; + return this; + } + + public Builder setSslPropertiesLocation(String sslPropertiesLocation) { + this.sslPropertiesLocation = sslPropertiesLocation; + return this; + } + + public Builder setKeyStore(String keyStore) { + this.keyStore = keyStore; + return this; + } + + public Builder setKeyStorePassphrase(String keyStorePassphrase) { + this.keyStorePassphrase = keyStorePassphrase; + return this; + } + + public Builder setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + return this; + } + + public Builder setTrustStore(String trustStore) { + this.trustStore = trustStore; + return this; + } + + public Builder setTrustStorePassphrase(String trustStorePassphrase) { + this.trustStorePassphrase = trustStorePassphrase; + return this; + } + + public Builder setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + return this; + } + + public Builder setSaslConfig(String saslConfig) { + this.saslConfig = saslConfig; + return this; + } + + public Builder setSenderPoolSize(int senderPoolSize) { + this.senderPoolSize = senderPoolSize; + return this; + } + + public Builder setMaxSenderRetries(int maxSenderRetries) { + this.maxSenderRetries = maxSenderRetries; + return this; + } + + public Builder setApplicationId(String applicationId) { + this.applicationId = applicationId; + return this; + } + + public Builder setRoutingKeyPattern(String routingKeyPattern) { + this.routingKeyPattern = routingKeyPattern; + return this; + } + + public Builder setGenerateId(boolean generateId) { + this.generateId = generateId; + return this; + } + + public Builder setDeliveryMode(String deliveryMode) { + this.deliveryMode = deliveryMode; + return this; + } + + public Builder setExchange(String exchange) { + this.exchange = exchange; + return this; + } + + public Builder setExchangeType(String exchangeType) { + this.exchangeType = exchangeType; + return this; + } + + public Builder setDeclareExchange(boolean declareExchange) { + this.declareExchange = declareExchange; + return this; + } + + public Builder setDurable(boolean durable) { + this.durable = durable; + return this; + } + + public Builder setAutoDelete(boolean autoDelete) { + this.autoDelete = autoDelete; + return this; + } + + public Builder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder setContentEncoding(String contentEncoding) { + this.contentEncoding = contentEncoding; + return this; + } + + public Builder setConnectionName(String connectionName) { + this.connectionName = connectionName; + return this; + } + + public Builder setClientConnectionProperties(String clientConnectionProperties) { + this.clientConnectionProperties = clientConnectionProperties; + return this; + } + + public Builder setAsync(boolean async) { + this.async = async; + return this; + } + + public Builder setCharset(String charset) { + this.charset = charset; + return this; + } + + public Builder setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + return this; + } + + public Builder setBlockingQueueFactory(BlockingQueueFactory blockingQueueFactory) { + this.blockingQueueFactory = blockingQueueFactory; + return this; + } + + public Builder setAddMdcAsHeaders(boolean addMdcAsHeaders) { + this.addMdcAsHeaders = addMdcAsHeaders; + return this; + } + + @Override + public AmqpAppender build() { + if (this.name == null) { + LOGGER.error("No name for AmqpAppender"); + } + Layout theLayout = this.layout; + if (theLayout == null) { + theLayout = PatternLayout.createDefaultLayout(); + } + AmqpManager manager = new AmqpManager(this.configuration.getLoggerContext(), this.name); + JavaUtils.INSTANCE + .acceptIfNotNull(this.uri, value -> manager.uri = value) + .acceptIfNotNull(this.host, value -> manager.host = value) + .acceptIfNotNull(this.port, value -> manager.port = Integers.parseInt(value)) + .acceptIfNotNull(this.addresses, value -> manager.addresses = value) + .acceptIfNotNull(this.user, value -> manager.username = value) + .acceptIfNotNull(this.password, value -> manager.password = value) + .acceptIfNotNull(this.virtualHost, value -> manager.virtualHost = value) + .acceptIfNotNull(this.useSsl, value -> manager.useSsl = value) + .acceptIfNotNull(this.verifyHostname, value -> manager.verifyHostname = value) + .acceptIfNotNull(this.sslAlgorithm, value -> manager.sslAlgorithm = value) + .acceptIfNotNull(this.sslPropertiesLocation, value -> manager.sslPropertiesLocation = value) + .acceptIfNotNull(this.keyStore, value -> manager.keyStore = value) + .acceptIfNotNull(this.keyStorePassphrase, value -> manager.keyStorePassphrase = value) + .acceptIfNotNull(this.keyStoreType, value -> manager.keyStoreType = value) + .acceptIfNotNull(this.trustStore, value -> manager.trustStore = value) + .acceptIfNotNull(this.trustStorePassphrase, value -> manager.trustStorePassphrase = value) + .acceptIfNotNull(this.trustStoreType, value -> manager.trustStoreType = value) + .acceptIfNotNull(this.saslConfig, value -> manager.saslConfig = value) + .acceptIfNotNull(this.senderPoolSize, value -> manager.senderPoolSize = value) + .acceptIfNotNull(this.maxSenderRetries, value -> manager.maxSenderRetries = value) + .acceptIfNotNull(this.applicationId, value -> manager.applicationId = value) + .acceptIfNotNull(this.routingKeyPattern, value -> manager.routingKeyPattern = value) + .acceptIfNotNull(this.generateId, value -> manager.generateId = value) + .acceptIfNotNull(this.deliveryMode, value -> manager.deliveryMode = MessageDeliveryMode.valueOf(this.deliveryMode)) + .acceptIfNotNull(this.exchange, value -> manager.exchangeName = value) + .acceptIfNotNull(this.exchangeType, value -> manager.exchangeType = value) + .acceptIfNotNull(this.declareExchange, value -> manager.declareExchange = value) + .acceptIfNotNull(this.durable, value -> manager.durable = value) + .acceptIfNotNull(this.autoDelete, value -> manager.autoDelete = value) + .acceptIfNotNull(this.contentType, value -> manager.contentType = value) + .acceptIfNotNull(this.contentEncoding, value -> manager.contentEncoding = value) + .acceptIfNotNull(this.connectionName, value -> manager.connectionName = value) + .acceptIfNotNull(this.clientConnectionProperties, value -> manager.clientConnectionProperties = value) + .acceptIfNotNull(this.charset, value -> manager.charset = value) + .acceptIfNotNull(this.async, value -> manager.async = value) + .acceptIfNotNull(this.addMdcAsHeaders, value -> manager.addMdcAsHeaders = value); + + BlockingQueue eventQueue; + if (this.blockingQueueFactory == null) { + eventQueue = new LinkedBlockingQueue<>(this.bufferSize); + } + else { + eventQueue = this.blockingQueueFactory.create(this.bufferSize); + } + + AmqpAppender appender = buildInstance(this.name, this.filter, theLayout, this.ignoreExceptions, manager, eventQueue); + if (manager.activateOptions()) { + appender.startSenders(); + return appender; + } + return null; + } + + /** + * Subclasses can extends Builder, use same logic but need to modify class instance. + * + * @param name The Appender name. + * @param filter The Filter to associate with the Appender. + * @param layout The layout to use to format the event. + * @param ignoreExceptions If true, exceptions will be logged and suppressed. If false errors will be logged and + * then passed to the application. + * @param manager Manager class for the appender. + * @param eventQueue Where LoggingEvents are queued to send. + * @return {@link AmqpAppender} + */ + protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { + return new AmqpAppender(name, filter, layout, ignoreExceptions, manager, eventQueue); + } + + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java index 19a4838e19..fc9e7a1295 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java @@ -116,6 +116,7 @@ public void test() { assertThat(threadName).isNotNull(); assertThat(threadName).isInstanceOf(String.class); assertThat(threadName).isEqualTo(Thread.currentThread().getName()); + ccf.destroy(); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppender.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppender.java new file mode 100644 index 0000000000..aca6034888 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppender.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 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 + * + * https://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.log4j2; + +import java.io.Serializable; +import java.util.concurrent.BlockingQueue; + +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; + +import org.springframework.amqp.core.Message; + +/** + * Extended Class for {@link AmqpAppender} + * @author Francesco Scipioni + * + * @since 2.2.4 + */ +@Plugin(name = "TestRabbitMQ", category = "Core", elementType = "appender", printObject = true) +public class ExtendAmqpAppender extends AmqpAppender { + + private String foo; + private String bar; + + public ExtendAmqpAppender(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue, String foo, String bar) { + super(name, filter, layout, ignoreExceptions, manager, eventQueue); + this.foo = foo; + this.bar = bar; + } + + @Override + protected Message postProcessMessageBeforeSend(Message message, Event event) { + message.getMessageProperties().setHeader(this.foo, this.bar); + return message; + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new ExtendBuilder(); + } + + protected static class ExtendBuilder extends Builder { + + @PluginBuilderAttribute("foo") + private String foo = "defaultFoo"; + + @PluginBuilderAttribute("bar") + private String bar = "defaultBar"; + + public Builder setFoo(String foo) { + this.foo = foo; + return this; + } + + public Builder setBar(String bar) { + this.bar = bar; + return this; + } + + @Override + protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { + return new ExtendAmqpAppender(name, filter, layout, ignoreExceptions, manager, eventQueue, this.foo, this.bar); + } + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java new file mode 100644 index 0000000000..a9d76fa568 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java @@ -0,0 +1,253 @@ +/* + * Copyright 2020 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 + * + * https://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.log4j2; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.FanoutExchange; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * @author Francesco Scipioni + * + * @since 2.2.4 + */ +@RabbitAvailable +public class ExtendAmqpAppenderTests { + + private static final LoggerContext LOGGER_CONTEXT = (LoggerContext) LogManager.getContext(false); + + private static final URI ORIGINAL_LOGGER_CONFIG = LOGGER_CONTEXT.getConfigLocation(); + + @BeforeAll + public static void setup() throws IOException { + LOGGER_CONTEXT.setConfigLocation(new ClassPathResource("log4j2-extend-amqp-appender.xml").getURI()); + LOGGER_CONTEXT.reconfigure(); + } + + @AfterAll + public static void teardown() { + LOGGER_CONTEXT.setConfigLocation(ORIGINAL_LOGGER_CONFIG); + LOGGER_CONTEXT.reconfigure(); + RabbitAvailableCondition.getBrokerRunning().deleteQueues("log4jTest", "log4j2Test"); + RabbitAvailableCondition.getBrokerRunning().deleteExchanges("log4j2Test", "log4j2Test_uri"); + } + + @Test + public void test() { + CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); + RabbitTemplate template = new RabbitTemplate(ccf); + RabbitAdmin admin = new RabbitAdmin(ccf); + FanoutExchange fanout = new FanoutExchange("log4j2Test"); + admin.declareExchange(fanout); + Queue queue = new Queue("log4j2Test"); + admin.declareQueue(queue); + admin.declareBinding(BindingBuilder.bind(queue).to(fanout)); + Logger logger = LogManager.getLogger("foo"); + logger.info("foo"); + logger.info("bar"); + template.setReceiveTimeout(10000); + Message received = template.receive(queue.getName()); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()).isEqualTo("testAppId.foo.INFO"); + Object foo = received.getMessageProperties().getHeader("foo"); + assertThat(foo).isNotNull(); + assertThat(foo).isInstanceOf(String.class); + assertThat(foo).isEqualTo("bar"); + // Cross-platform string comparison. Windows expects \n\r in the end of line + assertThat(new String(received.getBody())).startsWith("foo"); + received = template.receive(queue.getName()); + assertThat(received).isNotNull(); + assertThat(received.getMessageProperties().getReceivedRoutingKey()).isEqualTo("testAppId.foo.INFO"); + assertThat(new String(received.getBody())).startsWith("bar"); + Object threadName = received.getMessageProperties().getHeaders().get("thread"); + assertThat(threadName).isNotNull(); + assertThat(threadName).isInstanceOf(String.class); + assertThat(threadName).isEqualTo(Thread.currentThread().getName()); + foo = received.getMessageProperties().getHeaders().get("foo"); + assertThat(foo).isNotNull(); + assertThat(foo).isInstanceOf(String.class); + assertThat(foo).isEqualTo("bar"); + ccf.destroy(); + } + + @Test + public void testProperties() { + Logger logger = LogManager.getLogger("foo"); + AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", + Map.class).get("rabbitmq"); + Object manager = TestUtils.getPropertyValue(appender, "manager"); + // + // + assertThat(TestUtils.getPropertyValue(manager, "addresses")).isEqualTo("localhost:5672"); + assertThat(TestUtils.getPropertyValue(manager, "host")).isEqualTo("localhost"); + assertThat(TestUtils.getPropertyValue(manager, "port")).isEqualTo(5672); + assertThat(TestUtils.getPropertyValue(manager, "username")).isEqualTo("guest"); + assertThat(TestUtils.getPropertyValue(manager, "password")).isEqualTo("guest"); + assertThat(TestUtils.getPropertyValue(manager, "virtualHost")).isEqualTo("/"); + assertThat(TestUtils.getPropertyValue(manager, "exchangeName")).isEqualTo("log4j2Test"); + assertThat(TestUtils.getPropertyValue(manager, "exchangeType")).isEqualTo("fanout"); + assertThat(TestUtils.getPropertyValue(manager, "declareExchange", Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(manager, "durable", Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(manager, "autoDelete", Boolean.class)).isFalse(); + assertThat(TestUtils.getPropertyValue(manager, "applicationId")).isEqualTo("testAppId"); + assertThat(TestUtils.getPropertyValue(manager, "routingKeyPattern")).isEqualTo("%X{applicationId}.%c.%p"); + assertThat(TestUtils.getPropertyValue(manager, "contentType")).isEqualTo("text/plain"); + assertThat(TestUtils.getPropertyValue(manager, "contentEncoding")).isEqualTo("UTF-8"); + assertThat(TestUtils.getPropertyValue(manager, "generateId", Boolean.class)).isTrue(); + assertThat(TestUtils.getPropertyValue(manager, "deliveryMode")).isEqualTo(MessageDeliveryMode.NON_PERSISTENT); + assertThat(TestUtils.getPropertyValue(manager, "contentEncoding")).isEqualTo("UTF-8"); + assertThat(TestUtils.getPropertyValue(manager, "senderPoolSize")).isEqualTo(3); + assertThat(TestUtils.getPropertyValue(manager, "maxSenderRetries")).isEqualTo(5); + // change the property to true and this fails and test() randomly fails too. + assertThat(TestUtils.getPropertyValue(manager, "async", Boolean.class)).isFalse(); + // default value + assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); + + assertThat(TestUtils.getPropertyValue(appender, "events.items", Object[].class).length).isEqualTo(10); + + assertThat(TestUtils.getPropertyValue(appender, "foo")).isEqualTo("foo"); + assertThat(TestUtils.getPropertyValue(appender, "bar")).isEqualTo("bar"); + + Object events = TestUtils.getPropertyValue(appender, "events"); + assertThat(events.getClass()).isEqualTo(ArrayBlockingQueue.class); + } + + @Test + public void testAmqpAppenderEventQueueTypeDefaultsToLinkedBlockingQueue() { + Logger logger = LogManager.getLogger("default_queue_logger"); + AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", + Map.class).get("rabbitmq_default_queue"); + + Object events = TestUtils.getPropertyValue(appender, "events"); + + assertThat(TestUtils.getPropertyValue(appender, "foo")).isEqualTo("defaultFoo"); + assertThat(TestUtils.getPropertyValue(appender, "bar")).isEqualTo("defaultBar"); + + Object manager = TestUtils.getPropertyValue(appender, "manager"); + assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); + + assertThat(events.getClass()).isEqualTo(LinkedBlockingQueue.class); + } + + @Test + public void testUriProperties() { + Logger logger = LogManager.getLogger("bar"); + AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", + Map.class).get("rabbitmq_uri"); + Object manager = TestUtils.getPropertyValue(appender, "manager"); + assertThat(TestUtils.getPropertyValue(manager, "uri").toString()) + .isEqualTo("amqp://guest:guest@localhost:5672/"); + + assertThat(TestUtils.getPropertyValue(manager, "host")).isNull(); + assertThat(TestUtils.getPropertyValue(manager, "port")).isNull(); + assertThat(TestUtils.getPropertyValue(manager, "username")).isNull(); + assertThat(TestUtils.getPropertyValue(manager, "password")).isNull(); + assertThat(TestUtils.getPropertyValue(manager, "virtualHost")).isNull(); + assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isFalse(); + + assertThat(TestUtils.getPropertyValue(appender, "foo")).isEqualTo("foo_uri"); + assertThat(TestUtils.getPropertyValue(appender, "bar")).isEqualTo("bar_uri"); + } + + @Test + public void testDefaultConfiguration() { + @SuppressWarnings("resource") + AmqpAppender.AmqpManager manager = new ExtendAmqpAppender.AmqpManager(LOGGER_CONTEXT, "test"); + + RabbitConnectionFactoryBean bean = mock(RabbitConnectionFactoryBean.class); + manager.configureRabbitConnectionFactory(bean); + + verifyDefaultHostProperties(bean); + verify(bean, never()).setUseSSL(anyBoolean()); + } + + @Test + public void testCustomHostInformation() { + AmqpAppender.AmqpManager manager = new ExtendAmqpAppender.AmqpManager(LOGGER_CONTEXT, "test"); + + String host = "rabbitmq.com"; + int port = 5671; + String username = "user"; + String password = "password"; + String virtualHost = "vhost"; + + ReflectionTestUtils.setField(manager, "host", host); + ReflectionTestUtils.setField(manager, "port", port); + ReflectionTestUtils.setField(manager, "username", username); + ReflectionTestUtils.setField(manager, "password", password); + ReflectionTestUtils.setField(manager, "virtualHost", virtualHost); + + RabbitConnectionFactoryBean bean = mock(RabbitConnectionFactoryBean.class); + manager.configureRabbitConnectionFactory(bean); + + verify(bean).setHost(host); + verify(bean).setPort(port); + verify(bean).setUsername(username); + verify(bean).setPassword(password); + verify(bean).setVirtualHost(virtualHost); + } + + private void verifyDefaultHostProperties(RabbitConnectionFactoryBean bean) { + verify(bean, never()).setHost("localhost"); + verify(bean, never()).setPort(5672); + verify(bean, never()).setUsername("guest"); + verify(bean, never()).setPassword("guest"); + verify(bean, never()).setVirtualHost("/"); + } +} diff --git a/spring-rabbit/src/test/resources/log4j2-extend-amqp-appender.xml b/spring-rabbit/src/test/resources/log4j2-extend-amqp-appender.xml new file mode 100644 index 0000000000..e4d793629a --- /dev/null +++ b/spring-rabbit/src/test/resources/log4j2-extend-amqp-appender.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/reference/asciidoc/logging.adoc b/src/reference/asciidoc/logging.adoc index 9a600b2214..2579a7d647 100644 --- a/src/reference/asciidoc/logging.adoc +++ b/src/reference/asciidoc/logging.adoc @@ -270,6 +270,45 @@ public class MyEnhancedAppender extends AmqpAppender { ---- ==== +Starting with 2.2.4, the log4j2 `AmqpAppender` can be extended using `@PluginBuilderFactory` and extending also `AmqpAppender.Builder` + + +==== +[source, java] +---- +@Plugin(name = "MyEnhancedAppender", category = "Core", elementType = "appender", printObject = true) +public class MyEnhancedAppender extends AmqpAppender { + + public MyEnhancedAppender(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue, String foo, String bar) { + super(name, filter, layout, ignoreExceptions, manager, eventQueue); + + @Override + public Message postProcessMessageBeforeSend(Message message, Event event) { + message.getMessageProperties().setHeader("foo", "bar"); + return message; + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + protected static class Builder extends AmqpAppender.Builder { + + @Override + protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { + return new MyEnhancedAppender(name, filter, layout, ignoreExceptions, manager, eventQueue); + } + } + +} +---- +==== + + + ==== Customizing the Client Properties You can add custom client properties by adding either string properties or more complex properties. @@ -342,8 +381,6 @@ Then you can add `thing2` to logback.xml. For String properties such as those shown in the preceding example, the previous technique can be used. Subclasses allow for adding richer properties (such as adding a `Map` or numeric property). -With Log4j 2, subclasses are not supported, due to the way Log4j 2 uses static factory methods. - ==== Providing a Custom Queue Implementation The `AmqpAppenders` use a `BlockingQueue` to asynchronously publish logging events to RabbitMQ.