com.moonlit.logfaces.appenders.AsyncSocketAppender Maven / Gradle / Ivy
/**
* TCP socket appender using XML layout to be used with log4j logging framework.
* This is a derivitive work of Apache log4j project and adapted for logFaces.
* All credits go to the authors of log4j framework whose source code is re-used.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache Software License, Version 2.0
* which accompanies this distribution, and is available at
* http://logging.apache.org/log4j/1.2/license.html
*
* Copyright (c) 2010 Moonlit Software Ltd.
*/
package com.moonlit.logfaces.appenders;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.apache.log4j.Appender;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.spi.AppenderAttachable;
import org.apache.log4j.spi.LoggingEvent;
public class AsyncSocketAppender extends AppenderSkeleton implements AppenderAttachable{
public static final int DEFAULT_PORT = 55200;
public static final int DEFAULT_RECONNECTION_DELAY = 5000;
public static final int DEFAULT_QUEUE_SIZE = 500;
public static final int DEFAULT_NOF_RETRIES = 3;
public static final int DEFAULT_OFFER_TIMEOUT = 0;
public static final int DEFAULT_SHUTDOWN_TIMEOUT = 5000;
public static final String APPLICATION_KEY = "application";
public static final String HOSTNAME_KEY = "hostname";
protected String remoteHost;
protected InetAddress address;
protected int port = DEFAULT_PORT;
protected OutputStream oos;
protected boolean active;
protected int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
protected boolean locationInfo = true;
protected Connector connector;
protected String application, format, trustStore, trustStorePassword;
protected String backupFile;
protected Appender backupAppender;
protected BlockingQueue queue;
protected LogfacesLayout layout;
protected List hosts = new ArrayList();
protected Dispatcher dispatcher;
protected int hostIndex = 0;
protected long offerTimeout = DEFAULT_OFFER_TIMEOUT;
protected long shutdowdnTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
protected int nofRetries = DEFAULT_NOF_RETRIES;
protected int queueSize = DEFAULT_QUEUE_SIZE;
protected int nofFailures = 0;
protected int warnOverflow;
protected long totalCount;
protected SocketFactory socketFactory;
@Override
public boolean requiresLayout(){
return false;
}
@Override
public void activateOptions() {
if(hosts.size() == 0)
throw new IllegalStateException("remoteHost property is required for appender: " + name);
layout = new LogfacesLayout(application, locationInfo, "json".equals(format));
createSocketFactory();
// backward compatibility with log4j.porperties format which doesn't support
// appender references, the backup appender will be hard coded into RollingFileAppender
// using 'backupFile' property
if(backupFile != null && backupAppender == null){
try {
backupAppender = new RollingFileAppender(layout, backupFile, true);
backupAppender.setName("LFS-FALLBACK");
((RollingFileAppender)backupAppender).activateOptions();
} catch (Exception e) {
backupAppender = null;
System.err.println("logFaces: failed to init fall back appender: " + e.getMessage());
}
}
totalCount = 0;
queue = new ArrayBlockingQueue(queueSize, true);
dispatcher = new Dispatcher();
dispatcher.setName("LogfacesDispatcher");
dispatcher.setDaemon(true);
dispatcher.start();
while(!dispatcher.running){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
active = true;
new Thread(new Runnable(){
public void run() {
connect();
}
}).start();
}
@Override
public void close() {
if (closed)
return;
closed = true;
active = false;
try {
dispatcher.running = false;
dispatcher.join();
dispatcher = null;
} catch (Exception e) {
}
removeAllAppenders();
cleanUp();
}
public boolean isClosed(){
return closed;
}
public boolean isActive() {
return active;
}
protected void cleanUp() {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
System.err.println(String.format("logFaces: appender failed to close socket stream: %s", e.getMessage()));
}
oos = null;
}
if (connector != null) {
connector.shutdown = true;
connector.interrupt();
connector = null;
}
}
protected void connect() {
try {
cleanUp();
address = getAddressByName(hosts.get(hostIndex));
Socket socket = socketFactory.createSocket(address, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
oos = socket.getOutputStream();
}
catch (Exception e) {
System.err.println(String.format("logFaces: appender failed to connect to server %s:%d, starting failover", hosts.get(hostIndex), port));
startFailover();
}
}
@Override
public void append(LoggingEvent event) {
if (event == null || !active)
return;
try {
// copy original information of the caling thread
event.getNDC();
event.getThreadName();
event.getMDCCopy();
if(locationInfo)
event.getLocationInformation();
// try to queue
if(!queue.offer(event, offerTimeout, TimeUnit.MILLISECONDS)){
if(warnOverflow++ == 0){
System.err.println(String.format("logFaces: appender queue is full [%d]. If you see this message it means that queue size needs to be increased, or amount of log events decreased.", queue.size()));
System.err.println( (backupAppender == null)?"logFaces: fall back is disabled":String.format("logFaces backup appender %s activated; You can later import this data into the logfaces server manually.", backupAppender.getName()));
}
// transmition queue is full, delegate to fall back appender if specified
if(backupAppender != null)
backupAppender.doAppend(event);
}
else{
warnOverflow = 0;
}
}
catch(InterruptedException e){
}
catch(Exception e){
System.err.println(String.format("logFaces: appender failed to append, error: %s", e.getMessage()));
}
}
private InetAddress getAddressByName(String host) {
try {
return InetAddress.getByName(host);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected void startFailover() {
if (connector == null && nofRetries > 0 && active) {
address = getAddressByName(hosts.get(hostIndex));
System.err.println(String.format("logFaces: appender trying to fall back to %s", address));
connector = new Connector();
connector.setDaemon(true);
connector.setPriority(Thread.MIN_PRIORITY);
connector.start();
}
}
class Connector extends Thread {
volatile boolean shutdown = false;
public void run() {
Socket socket;
while (!shutdown) {
try {
sleep(reconnectionDelay);
socket = socketFactory.createSocket(address, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
synchronized (this) {
oos = socket.getOutputStream();
connector = null;
return;
}
} catch (InterruptedException e) {
return;
} catch(Exception e) {
if(++nofFailures >= nofRetries){
System.err.println(String.format("logFaces: appender unable to connect to %s after %d retries", address, nofRetries));
// fall back to next host in the list if retries are exhausted
if(++hostIndex >= hosts.size())
hostIndex = 0;
nofFailures = 0;
connector = null;
startFailover();
return;
}
}
}
}
}
private void createSocketFactory(){
socketFactory = SocketFactory.getDefault();
if(trustStore == null || trustStore.isEmpty())
return;
if(trustStorePassword == null || trustStorePassword.isEmpty())
return;
try {
TrustManager[] trustManagers;
SSLContext sslContext = SSLContext.getInstance("SSL");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream(trustStore), trustStorePassword.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
trustManagers = tmf.getTrustManagers();
sslContext.init(null, trustManagers, null);
socketFactory = sslContext.getSocketFactory();
} catch (Exception e) {
System.err.println(String.format("Failed to initialize SSL context: error: %s", e.getMessage()));
}
}
class Dispatcher extends Thread{
volatile boolean running = false;
public void run(){
LoggingEvent event;
running = true;
while(running || !queue.isEmpty()){
try {
if(oos == null){
Thread.sleep(500);
continue;
}
event = queue.poll(shutdowdnTimeout, TimeUnit.MILLISECONDS);
if(event == null)
continue;
}catch (InterruptedException e1) {
break;
}
catch(Exception e){
if(!running)
break;
System.err.println(String.format("logFaces appender queue taking failed: %s", e.getMessage()));
continue;
}
try{
// challenge few bytes to test broken connection
// without doing this, we may loose the event in socket buffers
oos.write(" ".getBytes());
oos.flush();
// transmit actual data
oos.write(layout.format(event).getBytes());
oos.flush();
totalCount++;
}
catch(IOException e){
oos = null;
System.err.println(String.format("logFaces appender socket write failed: %s", e.getMessage()));
if(!running)
break;
// put it back into queue and start recovery
queue.offer(event);
System.err.println(String.format("re-queued event, queue size now %d", queue.size()));
startFailover();
}
catch(Exception e){
if(!running)
break;
System.err.println(String.format("logFaces appender general purpose error: %s", e.getMessage()));
}
}
System.err.println("logFaces appender dispatcher thread ends");
}
}
public void setRemoteHost(String host) {
remoteHost = host;
String split[] = remoteHost.split(",");
hosts.addAll(Arrays.asList(split));
}
public String getRemoteHost() {
return remoteHost;
}
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return port;
}
public void setLocationInfo(boolean locationInfo) {
this.locationInfo = locationInfo;
}
public boolean getLocationInfo() {
return locationInfo;
}
public void setApplication(String lapp) {
this.application = lapp;
}
public String getApplication() {
return application;
}
public int getQueueSize() {
return queueSize;
}
public void setQueueSize(int queueSize) {
this.queueSize = queueSize;
}
public void setReconnectionDelay(int delay) {
if(delay <= DEFAULT_RECONNECTION_DELAY)
delay = DEFAULT_RECONNECTION_DELAY;
this.reconnectionDelay = delay;
}
public int getReconnectionDelay() {
return reconnectionDelay;
}
public int getNofRetries() {
return nofRetries;
}
public void setNofRetries(int nofRetries) {
this.nofRetries = nofRetries;
}
public long getOfferTimeout() {
return offerTimeout;
}
public void setOfferTimeout(long offerTimeout) {
this.offerTimeout = offerTimeout;
}
public long getShutdowdnTimeout() {
return shutdowdnTimeout;
}
public void setShutdowdnTimeout(long timeout) {
this.shutdowdnTimeout = timeout < DEFAULT_SHUTDOWN_TIMEOUT ? DEFAULT_SHUTDOWN_TIMEOUT : timeout;
}
public String getBackupFile() {
return backupFile;
}
public void setBackupFile(String backupFile) {
this.backupFile = backupFile;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public void setTrustStore(String trustStore) {
this.trustStore = trustStore;
}
public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
public String getTrustStore() {
return this.trustStore;
}
public String setTrustStorePassword() {
return this.trustStorePassword;
}
//
// custom implementation of AppenderAttachable
// we allow only single appender to be attached to this appender
// whatever new appender is attached/detached - it will be
// treated as a backupAppender automatically
//
@Override
public void addAppender(Appender appender) {
backupAppender = appender;
}
@SuppressWarnings("rawtypes")
@Override
public Enumeration getAllAppenders() {
return null;
}
@Override
public Appender getAppender(String arg0) {
return backupAppender;
}
@Override
public boolean isAttached(Appender appender) {
if(appender != null && appender.equals(backupAppender))
return true;
return false;
}
@Override
public void removeAllAppenders() {
if(backupAppender != null){
backupAppender.close();
backupAppender = null;
}
}
@Override
public void removeAppender(Appender appender) {
if(appender == backupAppender){
backupAppender.close();
backupAppender = null;
}
}
@Override
public void removeAppender(String name) {
if(backupAppender != null && backupAppender.getName().equals(name)){
backupAppender.close();
backupAppender = null;
}
}
public long getTotalCount() {
return totalCount;
}
}
© 2015 - 2026 Weber Informatics LLC | Privacy Policy