/*
* Copyright (C) 2012 by Pavlo Baron (pb at pbit dot org)
* Copyright (C) 2015 Commerce Technologies, Inc.
*
* 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 com.commercehub.logging.log4j.redis;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.util.SafeEncoder;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.*;
public class PooledRedisAppender extends AppenderSkeleton implements Runnable {
private JedisPoolConfig jedisPoolConfig;
private JedisPool jedisPool;
private String host = "localhost";
private int port = 6379;
private String password;
private String key;
private int batchSize = 100;
private long period = 500;
private boolean alwaysBatch = true;
private boolean purgeOnFailure = true;
private boolean daemonThread = true;
private int messageIndex = 0;
private Queue<LoggingEvent> events;
private byte[][] batch;
private ScheduledExecutorService executor;
private ScheduledFuture<?> task;
@Override
public void activateOptions() {
try {
super.activateOptions();
if (key == null) throw new IllegalStateException("Must set 'key'");
if (executor == null) executor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("RedisAppender", daemonThread));
if (task != null && !task.isDone()) task.cancel(true);
reInitializeJedisPool();
events = new ConcurrentLinkedQueue<>();
batch = new byte[batchSize][];
messageIndex = 0;
task = executor.scheduleWithFixedDelay(this, period, period, TimeUnit.MILLISECONDS);
} catch (Exception e) {
LogLog.error("Error during activateOptions", e);
}
}
private void reInitializeJedisPool() {
if (jedisPool != null) {
jedisPool.destroy();
}
if (jedisPoolConfig == null) {
jedisPoolConfig = new JedisPoolConfig();
}
jedisPool = new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT, password);
}
@Override
protected void append(LoggingEvent event) {
try {
populateEvent(event);
events.add(event);
} catch (Exception e) {
errorHandler.error("Error populating event and adding to queue", e, ErrorCode.GENERIC_FAILURE, event);
}
}
protected void populateEvent(LoggingEvent event) {
event.getThreadName();
event.getRenderedMessage();
event.getNDC();
event.getMDCCopy();
event.getThrowableStrRep();
}
@Override
public void close() {
try {
task.cancel(false);
executor.shutdown();
jedisPool.destroy();
} catch (Exception e) {
errorHandler.error(e.getMessage(), e, ErrorCode.CLOSE_FAILURE);
}
}
private Jedis getJedisFromPool() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
} catch (JedisConnectionException e) {
LogLog.error("Exception getting Jedis from pool", e);
}
return jedis;
}
@Override
public void run() {
try {
if (messageIndex == batchSize) push();
LoggingEvent event;
while ((event = events.poll()) != null) {
try {
String message = layout.format(event);
batch[messageIndex++] = SafeEncoder.encode(message);
} catch (Exception e) {
errorHandler.error(e.getMessage(), e, ErrorCode.GENERIC_FAILURE, event);
}
if (messageIndex == batchSize) push();
}
if (!alwaysBatch && messageIndex > 0) push();
} catch (Exception e) {
errorHandler.error(e.getMessage(), e, ErrorCode.WRITE_FAILURE);
}
}
private void push() {
Jedis jedis = getJedisFromPool();
if (jedis == null) {
purgeEventQueue();
return;
}
LogLog.debug("Sending " + messageIndex + " log messages to Redis");
try {
jedis.rpush(SafeEncoder.encode(key),
batchSize == messageIndex
? batch
: Arrays.copyOf(batch, messageIndex));
messageIndex = 0;
} catch (JedisConnectionException e) {
LogLog.error("Exception sending log messages to Redis.", e);
// returnBrokenResource when the state of the object is unrecoverable
jedisPool.returnBrokenResource(jedis);
jedis = null;
purgeEventQueue();
} finally {
// It's important to return the Jedis instance to the pool once you've finished using it
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
}
private void purgeEventQueue() {
if (purgeOnFailure) {
LogLog.debug("Purging event queue");
events.clear();
messageIndex = 0;
}
}
@SuppressWarnings("UnusedDeclaration")
public void setHost(String host) {
this.host = host;
}
@SuppressWarnings("UnusedDeclaration")
public void setPort(int port) {
this.port = port;
}
@SuppressWarnings("UnusedDeclaration")
public void setPassword(String password) {
this.password = password;
}
@SuppressWarnings("UnusedDeclaration")
public void setPeriod(long millis) {
this.period = millis;
}
public void setKey(String key) {
this.key = key;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
@SuppressWarnings("UnusedDeclaration")
public void setPurgeOnFailure(boolean purgeOnFailure) {
this.purgeOnFailure = purgeOnFailure;
}
@SuppressWarnings("UnusedDeclaration")
public void setAlwaysBatch(boolean alwaysBatch) {
this.alwaysBatch = alwaysBatch;
}
@SuppressWarnings("UnusedDeclaration")
public void setDaemonThread(boolean daemonThread){
this.daemonThread = daemonThread;
}
public boolean requiresLayout() {
return true;
}
public void setJedisPoolConfig(JedisPoolConfig config){
this.jedisPoolConfig = config;
}
// support testing
protected void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
}