Merge branch 'master' of https://github.com/nataliemeurer/logging-log4j2 into nataliemeurer-master
diff --git a/log4j-redis/pom.xml b/log4j-redis/pom.xml
new file mode 100644
index 0000000..85844eb
--- /dev/null
+++ b/log4j-redis/pom.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ~ Licensed to the Apache Software Foundation (ASF) under one or more ~ contributor license agreements. See the NOTICE
+ file distributed with ~ this work for additional information regarding copyright ownership. ~ The ASF licenses this file
+ to You 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. -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>log4j-redis</artifactId>
+ <name>Apache Log4j Redis</name>
+ <description>
+ Apache Log4j Redis.
+ </description>
+ <properties>
+ <log4jParentDir>${basedir}/..</log4jParentDir>
+ <docLabel>Log4j Redis Documentation</docLabel>
+ <projectDir>/log4j-redis</projectDir>
+ <module.name>org.apache.logging.log4j.redis</module.name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ </dependency>
+ <!-- Used for Redis appender -->
+ <dependency>
+ <groupId>redis.clients</groupId>
+ <artifactId>jedis</artifactId>
+ <version>2.9.0</version>
+ </dependency>
+ <!-- Test Dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <type>test-jar</type>
+ </dependency>
+ <!-- Mocking framework for use with JUnit -->
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <!-- Required for AsyncLoggers -->
+ <dependency>
+ <groupId>com.lmax</groupId>
+ <artifactId>disruptor</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Fragment-Host>org.apache.logging.log4j.core.appender.mom.redis</Fragment-Host>
+ <Export-Package>*</Export-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-changes-plugin</artifactId>
+ <version>${changes.plugin.version}</version>
+ <reportSets>
+ <reportSet>
+ <reports>
+ <report>changes-report</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ <configuration>
+ <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate>
+ <useJql>true</useJql>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>${checkstyle.plugin.version}</version>
+ <configuration>
+ <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> -->
+ <configLocation>${log4jParentDir}/checkstyle.xml</configLocation>
+ <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation>
+ <enableRulesSummary>false</enableRulesSummary>
+ <propertyExpansion>basedir=${basedir}</propertyExpansion>
+ <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <version>${javadoc.plugin.version}</version>
+ <configuration>
+ <bottom><![CDATA[<p align="center">Copyright © {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
+ Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
+ and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
+ <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+ <detectOfflineLinks>false</detectOfflineLinks>
+ <linksource>true</linksource>
+ </configuration>
+ <reportSets>
+ <reportSet>
+ <id>non-aggregate</id>
+ <reports>
+ <report>javadoc</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+ <plugin>
+ <groupId>com.github.spotbugs</groupId>
+ <artifactId>spotbugs-maven-plugin</artifactId>
+ <configuration>
+ <fork>true</fork>
+ <jvmArgs>-Duser.language=en</jvmArgs>
+ <threshold>Normal</threshold>
+ <effort>Default</effort>
+ <excludeFilterFile>${log4jParentDir}/spotbugs-exclude-filter.xml</excludeFilterFile>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jxr-plugin</artifactId>
+ <version>${jxr.plugin.version}</version>
+ <reportSets>
+ <reportSet>
+ <id>non-aggregate</id>
+ <reports>
+ <report>jxr</report>
+ </reports>
+ </reportSet>
+ <reportSet>
+ <id>aggregate</id>
+ <reports>
+ <report>aggregate</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-pmd-plugin</artifactId>
+ <version>${pmd.plugin.version}</version>
+ <configuration>
+ <targetJdk>${maven.compiler.target}</targetJdk>
+ </configuration>
+ </plugin>
+ </plugins>
+ </reporting>
+</project>
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java
new file mode 100644
index 0000000..ba3deac
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.core.Core;
+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 redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * Plugin configuration for {@link redis.clients.jedis.JedisPool} objects, allowing end users to set pool configuration
+ * if desired. If not set, will default to JedisPool defaults.
+ */
+@Plugin(name = "RedisPoolConfiguration", category = Core.CATEGORY_NAME, printObject = true)
+class LoggingRedisPoolConfiguration extends JedisPoolConfig {
+
+ private LoggingRedisPoolConfiguration() {
+ super();
+ }
+
+ static LoggingRedisPoolConfiguration defaultConfiguration() {
+ return LoggingRedisPoolConfiguration.newBuilder().build();
+ }
+
+ /**
+ * Creates a LoggingRedisPoolConfiguration from standard pool parameters.
+ */
+ @PluginBuilderFactory
+ public static Builder newBuilder() {
+ return new LoggingRedisPoolConfiguration.Builder();
+ }
+
+ private static class Builder implements org.apache.logging.log4j.core.util.Builder<LoggingRedisPoolConfiguration> {
+
+ @PluginBuilderAttribute("minIdle")
+ private int minIdle = JedisPoolConfig.DEFAULT_MIN_IDLE;
+
+ @PluginBuilderAttribute("maxIdle")
+ private int maxIdle = JedisPoolConfig.DEFAULT_MAX_IDLE;
+
+ @PluginBuilderAttribute("testOnBorrow")
+ private boolean testOnBorrow = JedisPoolConfig.DEFAULT_TEST_ON_BORROW;
+
+ @PluginBuilderAttribute("testOnReturn")
+ boolean testOnReturn = JedisPoolConfig.DEFAULT_TEST_ON_RETURN;
+
+ @PluginBuilderAttribute("testWhileIdle")
+ boolean testWhileIdle = JedisPoolConfig.DEFAULT_TEST_WHILE_IDLE;
+
+ @PluginBuilderAttribute("testsPerEvictionRun")
+ int testsPerEvictionRun = JedisPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
+
+ @PluginBuilderAttribute("timeBetweenEvictionRunsMillis")
+ long timeBetweenEvicationRunsMillis = JedisPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
+
+ @Override
+ public LoggingRedisPoolConfiguration build() {
+ LoggingRedisPoolConfiguration poolConfig = new LoggingRedisPoolConfiguration();
+ poolConfig.setMaxIdle(maxIdle);
+ poolConfig.setMinIdle(minIdle);
+ poolConfig.setTestOnBorrow(testOnBorrow);
+ poolConfig.setTestOnReturn(testOnReturn);
+ poolConfig.setTestWhileIdle(testWhileIdle);
+ poolConfig.setNumTestsPerEvictionRun(testsPerEvictionRun);
+ poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvicationRunsMillis);
+ return poolConfig;
+ }
+
+ public int getMinIdle() {
+ return minIdle;
+ }
+
+ public Builder setMinIdle(int minIdle) {
+ this.minIdle = minIdle;
+ return this;
+ }
+
+ public int getMaxIdle() {
+ return maxIdle;
+ }
+
+ public Builder setMaxIdle(int maxIdle) {
+ this.maxIdle = maxIdle;
+ return this;
+ }
+
+ public boolean isTestOnBorrow() {
+ return testOnBorrow;
+ }
+
+ public Builder setTestOnBorrow(boolean testOnBorrow) {
+ this.testOnBorrow = testOnBorrow;
+ return this;
+ }
+
+ public boolean isTestOnReturn() {
+ return testOnReturn;
+ }
+
+ public Builder setTestOnReturn(boolean testOnReturn) {
+ this.testOnReturn = testOnReturn;
+ return this;
+ }
+
+ public boolean isTestWhileIdle() {
+ return testWhileIdle;
+ }
+
+ public Builder setTestWhileIdle(boolean testWhileIdle) {
+ this.testWhileIdle = testWhileIdle;
+ return this;
+ }
+
+ public int getTestsPerEvictionRun() {
+ return testsPerEvictionRun;
+ }
+
+ public Builder setTestsPerEvictionRun(int testsPerEvictionRun) {
+ this.testsPerEvictionRun = testsPerEvictionRun;
+ return this;
+ }
+
+ public long getTimeBetweenEvicationRunsMillis() {
+ return timeBetweenEvicationRunsMillis;
+ }
+
+ public Builder setTimeBetweenEvicationRunsMillis(long timeBetweenEvicationRunsMillis) {
+ this.timeBetweenEvicationRunsMillis = timeBetweenEvicationRunsMillis;
+ return this;
+ }
+ }
+}
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java
new file mode 100644
index 0000000..4a19717
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.LoggingException;
+import org.apache.logging.log4j.core.*;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.appender.AppenderLoggingException;
+import org.apache.logging.log4j.core.config.Node;
+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.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.apache.logging.log4j.spi.AbstractLogger;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sends log events to a Redis key as a List. All logs are appended to Redis lists via the RPUSH command at keys defined
+ * in the configuration.
+ */
+@Plugin(name = "Redis", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
+public final class RedisAppender extends AbstractAppender {
+
+ // The default port here is the default port for Redis generally.
+ // For more details, see the full configuration: http://download.redis.io/redis-stable/redis.conf
+ private static final int DEFAULT_REDIS_PORT = 6379;
+ private static final String DEFAULT_REDIS_KEYS = "log-events";
+ private static final int DEFAULT_APPENDER_QUEUE_CAPACITY = 20;
+
+ private final RedisManager manager;
+ private final boolean immediateFlush;
+ private final LinkedBlockingQueue<String> logQueue;
+
+ private RedisAppender(final String name, final Layout<? extends Serializable> layout, final Filter filter,
+ final boolean ignoreExceptions, boolean immediateFlush, final int queueCapacity, final RedisManager manager) {
+ super(name, filter, layout, ignoreExceptions);
+ this.manager = Objects.requireNonNull(manager, "Redis Manager");
+ this.immediateFlush = immediateFlush;
+ this.logQueue = new LinkedBlockingQueue<>(queueCapacity);
+ }
+
+ /**
+ * Builds RedisAppender instances.
+ * @param <B> The type to build
+ */
+ public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
+ implements org.apache.logging.log4j.core.util.Builder<RedisAppender> {
+
+ private final String KEY_SEPARATOR = ",";
+
+ @PluginBuilderAttribute("host")
+ @Required(message = "No Redis hostname provided")
+ private String host;
+
+ @PluginBuilderAttribute("keys")
+ private String keys = DEFAULT_REDIS_KEYS;
+
+ @PluginBuilderAttribute("port")
+ private int port = DEFAULT_REDIS_PORT;
+
+ @PluginBuilderAttribute("immediateFlush")
+ private boolean immediateFlush = true;
+
+ @PluginBuilderAttribute("queueCapacity")
+ private int queueCapacity = DEFAULT_APPENDER_QUEUE_CAPACITY;
+
+ @PluginElement("SslConfiguration")
+ private SslConfiguration sslConfiguration;
+
+ @PluginElement("RedisPoolConfiguration")
+ private LoggingRedisPoolConfiguration poolConfiguration = LoggingRedisPoolConfiguration.defaultConfiguration();
+
+ @SuppressWarnings("resource")
+ @Override
+ public RedisAppender build() {
+ return new RedisAppender(
+ getName(),
+ getLayout(),
+ getFilter(),
+ isIgnoreExceptions(),
+ isImmediateFlush(),
+ getQueueCapacity(),
+ getRedisManager()
+ );
+ }
+
+ String getKeys() {
+ return keys;
+ }
+
+ String getHost() {
+ return host;
+ }
+
+ int getQueueCapacity() {
+ return queueCapacity;
+ }
+
+ boolean isImmediateFlush() {
+ return immediateFlush;
+ }
+
+ SslConfiguration getSslConfiguration() {
+ return sslConfiguration;
+ }
+
+ LoggingRedisPoolConfiguration getPoolConfiguration() {
+ return poolConfiguration;
+ }
+
+ int getPort() {
+ return port;
+ }
+
+ public B setKeys(final String keys) {
+ this.keys = keys;
+ return asBuilder();
+ }
+
+ public B setHost(final String host) {
+ this.host = host;
+ return asBuilder();
+ }
+
+ public B setPort(final int port) {
+ this.port = port;
+ return asBuilder();
+ }
+
+ public B setQueueCapacity(final int queueCapacity) {
+ this.queueCapacity = queueCapacity;
+ return asBuilder();
+ }
+
+ public B setPoolConfiguration(final LoggingRedisPoolConfiguration poolConfiguration) {
+ this.poolConfiguration = poolConfiguration;
+ return asBuilder();
+ }
+
+ public B setSslConfiguration(final SslConfiguration ssl) {
+ this.sslConfiguration = ssl;
+ return asBuilder();
+ }
+
+ public B setImmediateFlush(final boolean immediateFlush) {
+ this.immediateFlush = immediateFlush;
+ return asBuilder();
+ }
+
+ RedisManager getRedisManager() {
+ return new RedisManager(
+ getConfiguration().getLoggerContext(),
+ getName(),
+ getKeys().split(KEY_SEPARATOR),
+ getHost(),
+ getPort(),
+ getSslConfiguration(),
+ getPoolConfiguration()
+ );
+ }
+ }
+
+ /**
+ * Creates a builder for a RedisAppender.
+ * @return a builder for a RedisAppender.
+ */
+ @PluginBuilderFactory
+ public static <B extends Builder<B>> B newBuilder() {
+ return new Builder<B>().asBuilder();
+ }
+
+ @Override
+ public void append(final LogEvent event) {
+ final Layout<? extends Serializable> layout = getLayout();
+ if (event.getLoggerName() != null && AbstractLogger.getRecursionDepth() > 1) {
+ LOGGER.warn("Recursive logging from [{}] for appender [{}].", event.getLoggerName(), getName());
+ } else if (layout instanceof StringLayout) {
+ String serializedEvent = ((StringLayout)layout).toSerializable(event);
+ while (!logQueue.offer(serializedEvent)) {
+ tryFlushQueue();
+ }
+ if (shouldFlushLogQueue(event.isEndOfBatch())) {
+ tryFlushQueue();
+ }
+ } else {
+ throw new AppenderLoggingException("The Redis appender only supports StringLayouts.");
+ }
+ }
+
+ private boolean shouldFlushLogQueue(boolean endOfBatch) {
+ return immediateFlush || endOfBatch;
+ }
+
+ private void tryFlushQueue() {
+ List<String> logEvents = new ArrayList<>();
+ logQueue.drainTo(logEvents);
+ manager.sendBulk(logEvents);
+ }
+
+ @Override
+ public void start() {
+ setStarting();
+ manager.startup();
+ setStarted();
+ }
+
+ @Override
+ public boolean stop(final long timeout, final TimeUnit timeUnit) {
+ setStopping();
+ if (logQueue.size() > 0) {
+ tryFlushQueue();
+ }
+ boolean stopped = super.stop(timeout, timeUnit, false);
+ stopped &= manager.stop(timeout, timeUnit);
+ setStopped();
+ return stopped;
+ }
+
+ @Override
+ public String toString() {
+ return "RedisAppender{" +
+ "name=" + getName() +
+ ", host=" + manager.getHost() +
+ ", port=" + manager.getPort() +
+ ", keys=" + manager.getKeysAsString() +
+ ", immediateFlush=" + this.immediateFlush +
+ '}';
+ }
+}
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java
new file mode 100644
index 0000000..84eaadc
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractManager;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AutoCloseable wrapper class around a Redis client connection.
+ * Enables the transport of data to Redis lists via RPUSH to preconfigured keys.
+ */
+class RedisManager extends AbstractManager {
+
+ private final String[] keys;
+ private final String host;
+ private final int port;
+ private final SslConfiguration sslConfiguration;
+ private final JedisPoolConfig poolConfiguration;
+ private JedisPool jedisPool;
+
+ RedisManager(LoggerContext loggerContext, String name, String[] keys, String host, int port,
+ SslConfiguration sslConfiguration, LoggingRedisPoolConfiguration poolConfiguration) {
+ super(loggerContext, name);
+ this.keys = keys;
+ this.host = host;
+ this.port = port;
+ this.sslConfiguration = sslConfiguration;
+ if (poolConfiguration == null) {
+ this.poolConfiguration = LoggingRedisPoolConfiguration.defaultConfiguration();
+ } else {
+ this.poolConfiguration = poolConfiguration;
+ }
+ }
+
+ JedisPool createPool(String host, int port, SslConfiguration sslConfiguration) {
+ if (sslConfiguration != null) {
+ return new JedisPool(
+ poolConfiguration,
+ URI.create(host + ":" + port),
+ sslConfiguration.getSslSocketFactory(),
+ sslConfiguration.getSslContext().getSupportedSSLParameters(),
+ null
+ );
+ } else {
+ return new JedisPool(poolConfiguration, host, port, false);
+ }
+
+ }
+
+ public void startup() {
+ jedisPool = createPool(host, port, sslConfiguration);
+ }
+
+ public void sendBulk(List<String> logEvents) {
+ try (Jedis jedis = jedisPool.getResource()) {
+ if (!logEvents.isEmpty()) {
+ send(jedis, logEvents.toArray(new String[0]));
+ }
+ } catch (JedisConnectionException e) {
+ LOGGER.error("Unable to connect to redis. Please ensure that it's running on {}:{}", host, port, e);
+ }
+ }
+
+ public void send(String value) {
+ try (Jedis jedis = jedisPool.getResource()) {
+ try {
+ send(jedis, value);
+ } catch (JedisConnectionException e) {
+ LOGGER.error("Unable to connect to redis. Please ensure that it's running on {}:{}", host, port, e);
+ }
+ }
+ }
+
+ private void send(Jedis jedis, String... value) {
+ for (String key: keys) {
+ jedis.rpush(key, value);
+ }
+ }
+
+ @Override
+ protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {
+ if (jedisPool != null) {
+ jedisPool.destroy();
+ }
+ return true;
+ }
+
+ String getHost() {
+ return host;
+ }
+
+ int getPort() {
+ return port;
+ }
+
+ String getKeysAsString() {
+ return String.join(",", keys);
+ }
+}
diff --git a/log4j-redis/src/site/manual/index.md b/log4j-redis/src/site/manual/index.md
new file mode 100644
index 0000000..e371232
--- /dev/null
+++ b/log4j-redis/src/site/manual/index.md
@@ -0,0 +1,29 @@
+<!-- vim: set syn=markdown : -->
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+# Apache Log4j Redis module
+
+## Requirements
+
+Some features may require optional
+[dependencies](../runtime-dependencies.html). These dependencies are specified in the
+documentation for those features.
+
+Some Log4j features require external dependencies.
+See the [Dependency Tree](dependencies.html#Dependency_Tree)
+for the exact list of JAR files needed for these features.
diff --git a/log4j-redis/src/site/site.xml b/log4j-redis/src/site/site.xml
new file mode 100644
index 0000000..6d4cddc
--- /dev/null
+++ b/log4j-redis/src/site/site.xml
@@ -0,0 +1,52 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+
+-->
+<project name="Log4j Core"
+ xmlns="http://maven.apache.org/DECORATION/1.4.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+ <body>
+ <links>
+ <item name="Apache" href="http://www.apache.org/" />
+ <item name="Logging Services" href="http://logging.apache.org/"/>
+ <item name="Log4j" href="../index.html"/>
+ </links>
+
+ <!-- Component-specific reports -->
+ <menu ref="reports"/>
+
+ <!-- Overall Project Info -->
+ <menu name="Log4j Project Information" img="icon-info-sign">
+ <item name="Dependencies" href="../dependencies.html" />
+ <item name="Dependency Convergence" href="../dependency-convergence.html" />
+ <item name="Dependency Management" href="../dependency-management.html" />
+ <item name="Project Team" href="../team-list.html" />
+ <item name="Mailing Lists" href="../mail-lists.html" />
+ <item name="Issue Tracking" href="../issue-tracking.html" />
+ <item name="Project License" href="../license.html" />
+ <item name="Source Repository" href="../source-repository.html" />
+ <item name="Project Summary" href="../project-summary.html" />
+ </menu>
+
+ <menu name="Log4j Project Reports" img="icon-cog">
+ <item name="Changes Report" href="../changes-report.html" />
+ <item name="JIRA Report" href="../jira-report.html" />
+ <item name="Surefire Report" href="../surefire-report.html" />
+ <item name="RAT Report" href="../rat-report.html" />
+ </menu>
+ </body>
+</project>
diff --git a/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java
new file mode 100644
index 0000000..530a192
--- /dev/null
+++ b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LifeCycle;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.JedisPool;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests {@link RedisAppender}.
+ */
+public final class RedisAppenderTest {
+
+ private RedisAppender appender;
+ private Log4jLogEvent logEvent;
+ private RedisManager manager;
+
+ private String DESTINATION_KEY = "destination";
+ private String HOST = "localhost";
+ private int PORT = 6379;
+ private String MESSAGE = "Important Message";
+
+ @Before
+ public void setUp() {
+ initMocks();
+
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setImmediateFlush(true)
+ .withLayout(PatternLayout.createDefaultLayout())
+ .build();
+ logEvent = createLogEvent();
+ }
+
+ private void initMocks() {
+ manager = Mockito.mock(RedisManager.class);
+ when(manager.createPool(HOST, PORT, null)).thenReturn(Mockito.mock(JedisPool.class));
+ }
+
+ @Test
+ public void testAppenderStartsProperly() {
+ appender.start();
+ Mockito.verify(manager, Mockito.times(1)).startup();
+ assertEquals(appender.getState(), LifeCycle.State.STARTED);
+ }
+
+ @Test
+ public void testAppenderStopsProperly() {
+ appender.stop(500, TimeUnit.HOURS);
+ Mockito.verify(manager, Mockito.times(1)).stop(500, TimeUnit.HOURS);
+ assertEquals(appender.getState(), LifeCycle.State.STOPPED);
+ }
+
+ @Test
+ public void testAppendLogEvent() {
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+ }
+
+ @Test
+ public void testQueuesLogEvents() {
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setQueueCapacity(2)
+ .setImmediateFlush(false)
+ .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+ .build();
+
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(0)).sendBulk(any());
+ }
+
+ @Test
+ public void testAttemptsSendWhenQueueReachesCapacity() {
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setQueueCapacity(1)
+ .setImmediateFlush(false)
+ .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+ .build();
+
+ appender.append(logEvent);
+ appender.append(logEvent);
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(2)).sendBulk(anyList());
+ }
+
+ @Test
+ public void testFlushesQueueAtExceededCapacity() {
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setQueueCapacity(1)
+ .setImmediateFlush(false)
+ .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+ .build();
+
+ appender.append(logEvent);
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+ }
+
+ @Test
+ public void testFlushesQueueAtEndOfBatch() {
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setImmediateFlush(false)
+ .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+ .build();
+ logEvent = createPartialLogEvent().setEndOfBatch(true).build();
+
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+ }
+
+ @Test
+ public void testFlushesQueueOnAppenderStop() {
+ appender = new AppenderTestRedisAppenderBuilder()
+ .withName("RedisAppender")
+ .setKeys(DESTINATION_KEY)
+ .setHost(HOST)
+ .setPort(PORT)
+ .setImmediateFlush(false)
+ .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+ .build();
+ appender.append(logEvent);
+ Mockito.verify(manager, Mockito.times(0)).sendBulk(any());
+ appender.stop(100, TimeUnit.DAYS);
+ Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+ }
+
+ private Log4jLogEvent createLogEvent() {
+ return createPartialLogEvent().build();
+ }
+
+ private Log4jLogEvent.Builder createPartialLogEvent() {
+ return Log4jLogEvent.newBuilder()
+ .setLoggerName(RedisAppenderTest.class.getName())
+ .setLoggerFqcn(RedisAppenderTest.class.getName())
+ .setLevel(Level.INFO)
+ .setMessage(new SimpleMessage(MESSAGE));
+ }
+
+ private class AppenderTestRedisAppenderBuilder extends RedisAppender.Builder<AppenderTestRedisAppenderBuilder> {
+ @Override
+ RedisManager getRedisManager() {
+ return manager;
+ }
+ }
+}
diff --git a/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java
new file mode 100644
index 0000000..0a68f9c
--- /dev/null
+++ b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+public class RedisManagerTest {
+
+ private JedisPool mockJedisPool;
+ private Jedis mockJedis;
+ private RedisManager manager;
+
+ private String KEYS = "abc,def";
+
+ @Before
+ public void setUp() {
+ initMocks();
+ manager = new ManagerTestRedisAppenderBuilder()
+ .setHost("localhost")
+ .setPort(6379)
+ .setKeys(KEYS)
+ .getRedisManager();
+ manager.startup();
+ }
+
+ private void initMocks() {
+ mockJedisPool = Mockito.mock(JedisPool.class);
+ mockJedis = Mockito.mock(Jedis.class);
+ when(mockJedisPool.getResource()).thenReturn(mockJedis);
+ when(mockJedis.rpush(anyString(), anyString())).thenReturn(1L);
+ }
+
+ @Test
+ public void testSendsValuesToAllKeys() {
+ manager.send("value");
+ Mockito.verify(mockJedisPool).getResource();
+ Mockito.verify(mockJedis, Mockito.times(KEYS.split(",").length)).rpush(anyString(), anyString());
+ for (String k: KEYS.split(",")) {
+ Mockito.verify(mockJedis, Mockito.times(1)).rpush(eq(k), anyString());
+ }
+ }
+
+ @Test
+ public void testReleasePoolResources() {
+ manager.stop(100, TimeUnit.HOURS);
+ Mockito.verify(mockJedisPool).destroy();
+ }
+
+ @Test
+ public void testSendsAllValuesInBulk() {
+ List<String> logs = new ArrayList<>();
+ logs.add("value1");
+ logs.add("value2");
+ manager.sendBulk(logs);
+ Mockito.verify(mockJedis, Mockito.times(KEYS.split(",").length)).rpush(anyString(), eq("value1"), eq("value2"));
+ }
+
+ private class TestRedisManager extends RedisManager {
+
+ TestRedisManager(LoggerContext loggerContext, String name, String keys, String host, int port) {
+ super(loggerContext, name, keys.split(","), host, port, null, null);
+ }
+
+ @Override
+ JedisPool createPool(String host, int port, SslConfiguration ssl) {
+ return mockJedisPool;
+ }
+ }
+
+ private class ManagerTestRedisAppenderBuilder extends RedisAppender.Builder<ManagerTestRedisAppenderBuilder> {
+ @Override
+ RedisManager getRedisManager() {
+ return new TestRedisManager(
+ LoggerContext.getContext(),
+ getName(),
+ getKeys(),
+ getHost(),
+ getPort()
+ );
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 04239367..55b372e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1557,6 +1557,7 @@
<module>log4j-jeromq</module>
<module>log4j-jms</module>
<module>log4j-kafka</module>
+ <module>log4j-redis</module>
<module>log4j-couchdb</module>
<module>log4j-mongodb3</module>
<module>log4j-mongodb4</module>
diff --git a/src/site/asciidoc/manual/appenders.adoc b/src/site/asciidoc/manual/appenders.adoc
index 9476517..5fc0bf3 100644
--- a/src/site/asciidoc/manual/appenders.adoc
+++ b/src/site/asciidoc/manual/appenders.adoc
@@ -2268,6 +2268,97 @@
</Configuration>
----
+
+[#RedisAppender]
+== RedisAppender
+
+Log4j2 supports a RedisAppender as part of the `log4j2-redis` module. The RedisAppender logs events to a https://redis.io/[Redis] queue. Each log event is appended via the standard
+https://redis.io/commands/rpush[RPUSH] command.
+
+.RedisAppender Parameters
+[cols=",,",options="header",]
+|=======================================================================
+|Parameter Name |Type |Description
+
+|name |String |The name of the Appender. Required.
+
+|layout |Layout |The Layout to use to format the LogEvent. Required,
+there is no default. _New since 2.9, in previous versions <PatternLayout
+pattern="%m"/> was default._
+
+|filter |Filter |A Filter to determine if the event should be handled by
+this Appender. More than one Filter may be used by using a
+CompositeFilter.
+
+|host |String |The hostname on which Redis is running (required).
+
+|port |int |The port on which Redis is running. Default: 6379.
+
+|keys |String |The keys of the queue to which each log message should be sent, separated by commas (e.g. "key-one,key-two,key-three"). Default: "logEvents".
+
+|ignoreExceptions |boolean |The default is `true`, causing exceptions
+encountered while appending events to be internally logged and then
+ignored. When set to `false` exceptions will be propagated to the
+caller, instead. You must set this to `false` when wrapping this
+Appender in a link:#FailoverAppender[FailoverAppender].
+
+|immediateFlush | boolean | Whether to immediately send logs to redis rather than sending at the end of a batch. Default: true.
+
+|queueCapacity | int | The maximum number of logs to hold in memory before sending to Redis. Default: 20.
+
+|SslConfiguration |SslConfiguration |Contains the configuration for the KeyStore and
+TrustStore. See link:#SSL[SSL].
+
+|RedisPoolConfiguration |LoggingRedisPoolConfiguration |Granular configuration of the resource pool for the Redis
+client connection. Available parameters include maxIdle, minIdle, testOnBorrow, testOnReturn, testWhileIdle, numTestsPerEvictionRun, and timeBetweenEvictionRunsMillis.
+|=======================================================================
+
+Here is a sample RedisAppender configuration snippet in XML:
+
+[source,prettyprint,linenums]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+ ...
+ <Appenders>
+ <Redis name"RedisAppend" host="localhost" port=6379 keys="key-one,key-two" immediateFlush=false>
+ <PatternLayout>
+ <Pattern>%m</Pattern>
+ </PatternLayout>
+ </Redis>
+ <Async name="Async">
+ <AppenderRef ref="RedisAppend"/>
+ </Async>
+ </Appenders>
+----
+You can also configure the appender easily in YML:
+[source,prettyprint,linenums]
+----
+Configuration:
+ Appenders:
+ Redis:
+ name: RedisAppend
+ host: localhost
+ port: 6379
+ Keys: key-one,key-two
+ PatternLayout:
+ pattern: "%m"
+----
+
+
+This appender operates synchronously by default and will block until the record
+has been acknowledged by the Redis server. Wrap with
+http://logging.apache.org/log4j/2.x/manual/appenders.html#AsyncAppender[Async
+appender] and/or set syncSend to `false` to log asynchronously (as in the first example above).
+
+=== Redis vs. Kafka
+
+In a logging context, Redis may play a similar role to Kafka, serving as a message broker
+in logging pipelines. In general, Redis may be preferred to Kafka in situations where
+an in-memory queue is desired. A fairly simple breakdown of the diffferent approaches can be found
+https://stackoverflow.com/questions/37990784/difference-between-redis-and-kafka[on StackOverflow here].
+
+If you prefer to use Kafka, please refer to the supported <<KafkaAppender>>.
+
[#RewriteAppender]
== RewriteAppender
diff --git a/src/site/asciidoc/runtime-dependencies.adoc b/src/site/asciidoc/runtime-dependencies.adoc
index c0407f7..e13b82c 100644
--- a/src/site/asciidoc/runtime-dependencies.adoc
+++ b/src/site/asciidoc/runtime-dependencies.adoc
@@ -172,7 +172,7 @@
|http://lmax-exchange.github.io/disruptor/[LMAX Disruptor]
|Kafka Appender
-a|http://kafka.apache.org/[Kafka client library].
+|http://kafka.apache.org/[Kafka client library].
[NOTE]
====
You need to use a version of the Kafka client library matching the Kafka server used.
@@ -200,6 +200,9 @@
|NoSQL Appender with Apache CouchDB provider
|LightCouch CouchDB client library and Log4j CouchDB library
+|Redis Appender
+|https://github.com/xetorthio/jedis[Jedis client]
+
|Cassandra Appender
|Datastax Cassandra driver and Log4j Cassandra library
diff --git a/src/site/site.xml b/src/site/site.xml
index 49437e8..48a6d3e 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -181,6 +181,7 @@
<item name="NoSQL for CouchDB" href="/manual/appenders.html#NoSQLAppenderCouchDB"/>
<item name="Output Stream" href="/manual/appenders.html#OutputStreamAppender"/>
<item name="Random Access File" href="/manual/appenders.html#RandomAccessFileAppender"/>
+ <item name="Redis" href="/manual/appenders.html#RedisAppender"/>
<item name="Rewrite" href="/manual/appenders.html#RewriteAppender"/>
<item name="Rolling File" href="/manual/appenders.html#RollingFileAppender"/>
<item name="Rolling Random Access File" href="/manual/appenders.html#RollingRandomAccessFileAppender"/>