package uk.co.appembassy.log4mqtt; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.spi.ErrorCode; import org.apache.log4j.spi.LoggingEvent; import org.eclipse.paho.client.mqttv3.*; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.locks.Lock; public class MqttAppender extends AppenderSkeleton implements MqttCallback { private MqttClient mqtt; private String hostname; private String ip; private final int RECONNECT_MIN = 1000; private final int RECONNECT_MAX = 32000; private int currentReconnect = 1000; private boolean connected = false; private LinkedBlockingQueue<LoggingEvent> queue = new LinkedBlockingQueue<LoggingEvent>(10000); private String broker; private String clientid; private String username; private String password; private int connectionTimeout = 2000; private int keepAliveInterval = 60000; private String topic; private int qos = 0; private boolean retain = false; private String outputFormat = "json"; public String getBroker() { return broker; } public void setBroker(String broker) { this.broker = broker; } public String getClientid() { return clientid; } public void setClientid(String clientid) { this.clientid = clientid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getConnectionTimeout() { return connectionTimeout; } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getKeepAliveInterval() { return keepAliveInterval; } public void setKeepAliveInterval(int keepAliveInterval) { this.keepAliveInterval = keepAliveInterval; } public String getTopic() { return topic; } public void setTopic(String topic) { this.topic = topic; } public int getQos() { return qos; } public void setQos(int qos) { this.qos = qos; } public boolean isRetain() { return retain; } public void setRetain(boolean retain) { this.retain = retain; } public String getOutputFormat() { return outputFormat; } public void setOutputFormat(String outputFormat) { this.outputFormat = outputFormat; } private void connectMqtt() { MqttConnectOptions opts = new MqttConnectOptions(); opts.setConnectionTimeout(connectionTimeout); opts.setKeepAliveInterval(keepAliveInterval); if ( username != null ) { opts.setUserName(username); } if ( password != null ) { opts.setPassword(password.toCharArray()); } try { mqtt = new MqttClient(broker, clientid, null); mqtt.connect(opts); mqtt.setCallback(this); currentReconnect = RECONNECT_MIN; synchronized (this) { connected = true; } emptyQueue(); } catch (MqttSecurityException ex1) { errorHandler.error("MQTT Security error: " + ex1); } catch (MqttException ex2) { int code = ex2.getReasonCode(); switch (code) { case 0: // connection successful, am I ever going here? // while testing I've noticed I am as I was receiving code 0 here, weird... currentReconnect = RECONNECT_MIN; synchronized (this) { connected = true; } emptyQueue(); break; case 1: errorHandler.error("MQTT connection error: Connection Refused: unacceptable protocol version"); break; case 2: errorHandler.error("MQTT connection error: Connection Refused: identifier ("+clientid+") rejected"); break; case 3: errorHandler.error("MQTT connection error: Connection Refused: server unavailable"); break; case 4: errorHandler.error("MQTT connection error: Connection Refused: bad user name or password"); break; case 5: errorHandler.error("MQTT connection error: Connection Refused: not authorized"); break; default: errorHandler.error("MQTT connection error: Unknown response -> " + code + ", reconnecting..."); reconnectMqtt(); } } } private synchronized void emptyQueue() { while (!queue.isEmpty()) { append(queue.poll()); } } private void reconnectMqtt() { if ( currentReconnect < RECONNECT_MAX ) { currentReconnect += currentReconnect; } Thread t = new Thread() { public void run() { try { Thread.sleep(currentReconnect); connectMqtt(); } catch (InterruptedException ex) {} } }; t.start(); } public boolean requiresLayout() { return true; } public void activateOptions() { if ( outputFormat.equals("json") ) { this.setLayout(new JsonLoggingEventLayout()); } else if ( outputFormat.equals("xml") ) { this.setLayout(new XmlLoggingEventLayout()); } else { errorHandler.error("Unknown outputFormat " + outputFormat + " in " + name, null, ErrorCode.MISSING_LAYOUT ); return; } this.layout.activateOptions(); // not sure if I have to call this manually? try { hostname = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException ex) { hostname = "<unknown>"; } try { ip = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException ex) { ip = "<unknown>"; } if ( clientid.indexOf("{ip}") > -1 ) { clientid = clientid.replace("{ip}".subSequence(0,"{ip}".length()), ip.subSequence(0,ip.length())); } else if ( clientid.indexOf("{hostname}") > -1 ) { clientid = clientid.replace("{hostname}".subSequence(0, "{hostname}".length()), hostname.subSequence(0, hostname.length())); } connectMqtt(); } public void finalize() { this.close(); } public synchronized void append( LoggingEvent event ) { if( this.layout == null ){ errorHandler.error("No layout for appender " + name, null, ErrorCode.MISSING_LAYOUT ); return; } if ( connected ) { MqttMessage msg = new MqttMessage(); msg.setPayload( this.layout.format(event).getBytes() ); msg.setQos( qos ); msg.setRetained( retain ); try { if ( mqtt != null ) { mqtt.getTopic(this.topic).publish(msg); } } catch (MqttPersistenceException ex1) { errorHandler.error("MQTT Could not send a message: " + ex1); if ( !queue.offer(event) ) { errorHandler.error("MQTT offer queue is full. Messages will be lost."); } } catch (MqttException ex2) { errorHandler.error("MQTT Could not send a message: " + ex2); if ( !queue.offer(event) ) { errorHandler.error("MQTT offer queue is full. Messages will be lost."); } } } else { if ( !queue.offer(event) ) { errorHandler.error("MQTT offer queue is full. Messages will be lost."); } } } public synchronized void close() { synchronized (this) { connected = false; } try { mqtt.disconnect(); } catch (MqttException ex) { errorHandler.error("Could not disconnect the MQTT client: " + ex); } finally { mqtt = null; } } @Override public void connectionLost(Throwable cause) { this.close(); errorHandler.error("Connection to the MQTT broker lost, reconnecting: " + cause); reconnectMqtt(); } @Override public void messageArrived(MqttTopic topic, MqttMessage message) throws Exception { // we are not receiving any messages here... } @Override public void deliveryComplete(MqttDeliveryToken token) { // we are not receiving any messages here... } }