net.logstash.logback.appender.AbstractLogstashTcpSocketAppender Maven / Gradle / Ivy
Show all versions of logstash-logback-encoder Show documentation
/**
* 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 net.logstash.logback.appender;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import net.logstash.logback.Logback11Support;
import net.logstash.logback.appender.destination.DelegateDestinationConnectionStrategy;
import net.logstash.logback.appender.destination.DestinationConnectionStrategy;
import net.logstash.logback.appender.destination.DestinationParser;
import net.logstash.logback.appender.destination.PreferPrimaryDestinationConnectionStrategy;
import net.logstash.logback.appender.listener.TcpAppenderListener;
import net.logstash.logback.encoder.SeparatorParser;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.joran.spi.DefaultClass;
import ch.qos.logback.core.net.ssl.ConfigurableSSLSocketFactory;
import ch.qos.logback.core.net.ssl.SSLConfigurableSocket;
import ch.qos.logback.core.net.ssl.SSLConfiguration;
import ch.qos.logback.core.net.ssl.SSLParametersConfiguration;
import ch.qos.logback.core.spi.DeferredProcessingAware;
import ch.qos.logback.core.util.CloseUtil;
import ch.qos.logback.core.util.Duration;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.LifecycleAware;
import com.lmax.disruptor.RingBuffer;
/**
* An {@link AsyncDisruptorAppender} appender that writes
* events to a TCP {@link Socket} outputStream.
*
*
* The behavior is similar to a {@link ch.qos.logback.classic.net.SocketAppender}, except that:
*
* - it uses a {@link RingBuffer} instead of a {@link BlockingQueue}
* - it writes using an {@link Encoder} instead of serialization
*
*
*
* In addition, SSL can be enabled by setting the SSL configuration via {@link #setSsl(SSLConfiguration)}.
* See the logback manual
* for details on how to configure client-side SSL.
*
* @author Mirko Bernardoni (original, which did not use disruptor)
* @since 11 Jun 2014 (creation date)
*/
public abstract class AbstractLogstashTcpSocketAppender>
extends AsyncDisruptorAppender {
protected static final String HOST_NAME_FORMAT = "%3$s";
protected static final String PORT_FORMAT = "%4$d";
public static final String DEFAULT_THREAD_NAME_FORMAT = "logback-appender-" + APPENDER_NAME_FORMAT + "-" + HOST_NAME_FORMAT + ":" + PORT_FORMAT + "-" + THREAD_INDEX_FORMAT;
/**
* The default port number of remote logging server (4560).
*/
public static final int DEFAULT_PORT = 4560;
/**
* The default reconnection delay (30000 milliseconds or 30 seconds).
*/
public static final int DEFAULT_RECONNECTION_DELAY = 30000;
/**
* The default write timeout in milliseconds (0 means no write timeout).
*/
public static final int DEFAULT_WRITE_TIMEOUT = 0;
/**
* Default size of the queue used to hold logging events that are destined
* for the remote peer.
* Assuming an average log entry to take 1k, this would result in the application
* using about 10MB additional memory if the queue is full
*/
public static final int DEFAULT_QUEUE_SIZE = DEFAULT_RING_BUFFER_SIZE;
/**
* Default timeout when waiting for the remote server to accept our
* connection.
*/
public static final int DEFAULT_CONNECTION_TIMEOUT = 5000;
public static final int DEFAULT_WRITE_BUFFER_SIZE = 8192;
private static final NotConnectedException NOT_CONNECTED_EXCEPTION = new NotConnectedException();
private static final ShutdownInProgressException SHUTDOWN_IN_PROGRESS_EXCEPTION = new ShutdownInProgressException();
static {
NOT_CONNECTED_EXCEPTION.setStackTrace(new StackTraceElement[] { new StackTraceElement(AbstractLogstashTcpSocketAppender.TcpSendingEventHandler.class.getName(), "onEvent(..)", null, -1)});
SHUTDOWN_IN_PROGRESS_EXCEPTION.setStackTrace(new StackTraceElement[] { new StackTraceElement(AbstractLogstashTcpSocketAppender.TcpSendingEventHandler.class.getName(), "onEvent(..)", null, -1)});
}
/**
* The host to which to connect and send events
*/
private String remoteHost;
/**
* The TCP port on the host to which to connect and send events
*/
private int port = DEFAULT_PORT;
/**
* Destinations to which to attempt to send logs, in order of preference.
*
*
* Logs are only sent to one destination at a time.
*
*
* The interpretation of this list is up to the current {@link #connectionStrategy}.
*/
private List destinations = new ArrayList<>(2);
/**
* When connected, this is the index into {@link #destinations}
* to the currently connected destination.
*
*
* When a connection has never been established, the value is 0.
*
*
* When a connection has been established, but lost, the value is the
* previously connected index.
*/
private volatile int connectedDestinationIndex = 0;
/**
* When connected, this is the connected destination address.
* When not connected, this is null.
*/
private volatile InetSocketAddress connectedDestination;
/**
* Strategy used to determine to which destination to connect, and when to reconnect.
* Default is {@link PreferPrimaryDestinationConnectionStrategy}.
*/
private DestinationConnectionStrategy connectionStrategy = new PreferPrimaryDestinationConnectionStrategy();
/**
* Time period for which to wait after a connection fails to a specific destination
* before attempting to reconnect to that destination.
* Default is {@value #DEFAULT_RECONNECTION_DELAY} milliseconds.
*/
private Duration reconnectionDelay = new Duration(DEFAULT_RECONNECTION_DELAY);
/**
* Socket connection timeout in milliseconds.
*/
private int acceptConnectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
/**
* Human readable identifier of the client (used for logback status messages)
*/
private String peerId;
/**
* The encoder which is ultimately responsible for writing the event
* to the socket's {@link java.io.OutputStream}.
*/
private Encoder encoder;
/**
* The number of bytes available in the write buffer.
* Defaults to {@value #DEFAULT_WRITE_BUFFER_SIZE}
*
* If less than or equal to zero, buffering the output stream will be disabled.
* If buffering is disabled, the writer thread can slow down, but
* it will also can prevent dropping events in the buffer on flaky connections.
*/
private int writeBufferSize = DEFAULT_WRITE_BUFFER_SIZE;
/**
* Used to create client {@link Socket}s to which to communicate.
*
* If set prior to startup, it will be used.
*
*
* If not set prior to startup, and {@link #sslConfiguration} is null,
* then the default socket factory ({@link SocketFactory#getDefault()}) will be used.
*
*
* If not set prior to startup, and {@link #sslConfiguration} is not null,
* then a socket factory created from the
* {@link SSLConfiguration#createContext(ch.qos.logback.core.spi.ContextAware)} will be used.
*/
private SocketFactory socketFactory;
/**
* Set this to non-null to use SSL.
* See the logback manual
* for details on how to configure SSL for a client.
*/
private SSLConfiguration sslConfiguration;
/**
* If this duration elapses without an event being sent,
* then the {@link #keepAliveMessage} will be sent to the socket in
* order to keep the connection alive.
*
* When null (the default), no keepAlive messages will be sent.
*/
private Duration keepAliveDuration;
/**
* Message to send for keeping the connection alive
* if {@link #keepAliveDuration} is non-null.
*/
private String keepAliveMessage = System.getProperty("line.separator");
/**
* The charset to use when writing the {@link #keepAliveMessage}.
* Defaults to UTF-8.
*/
private Charset keepAliveCharset = StandardCharsets.UTF_8;
/**
* The {@link #keepAliveMessage} translated to bytes using the {@link #keepAliveCharset}.
* Populated at startup time.
*/
private byte[] keepAliveBytes;
/**
* Time period for which to wait for a write to complete before timing out
* and attempting to reconnect to that destination.
* Zero (the default) means no write timeout.
*
*
Used to detect connections where the receiver stops reading.
*
* Note that since a blocking java socket output stream
* does not have a concept of a write timeout,
* a task will be scheduled on the {@link #getExecutorService()}
* with the same frequency as the write timeout
* in order to detect stuck writes.
* It is recommended to use longer write timeouts (e.g. > 30s, or minutes),
* rather than short write timeouts, so that this task does not execute too frequently.
* Also, this approach means that it could take up to two times the write timeout
* before a write timeout is detected.
*/
private Duration writeTimeout = new Duration(DEFAULT_WRITE_TIMEOUT);
/**
* Used to signal the socket reconnect thread that the shutdown has occurred.
* The latch will be non-zero when started, and zero when shutdown.
*/
private volatile CountDownLatch shutdownLatch;
/**
* Event handler responsible for performing the TCP transmission.
*/
private class TcpSendingEventHandler implements EventHandler>, LifecycleAware {
/**
* Max number of consecutive failed connection attempts for which
* logback status messages will be logged.
*
* After this many failed attempts, reconnection will still
* be attempted, but failures will not be logged again
* (until after the connection is successful, and then fails again.)
*/
private static final int MAX_REPEAT_CONNECTION_ERROR_LOG = 5;
/**
* Number of times we try to write an event before it is discarded.
* Between each attempt, the socket will be reconnected.
*/
private static final int MAX_REPEAT_WRITE_ATTEMPTS = 5;
/**
* The destination socket to which to send events.
*/
private volatile Socket socket;
/**
* The destination output stream to which to send events.
* If {@link AbstractLogstashTcpSocketAppender#writeBufferSize} is greater than zero, this will be a buffered wrapper of the socket output stream.
* Otherwise, it will be the socket output stream.
*/
private volatile OutputStream outputStream;
/**
* Time at which the last event send was started (e.g. before write/flush).
* Used to detect write timeouts.
*/
private volatile long lastSendStartNanoTime;
/**
* Time at which the last event send was completed (e.g. after write/flush).
* Used to calculate if a keep alive message
* needs to be scheduled/sent.
*/
private volatile long lastSendEndNanoTime;
/**
* The most recent time that a connection to each destination was attempted.
*/
private long[] destinationAttemptStartTimes;
/**
* Future for the currently scheduled {@link #keepAliveRunnable}.
*/
private ScheduledFuture> keepAliveFuture;
/**
* See {@link KeepAliveRunnable}.
* Initialized on startup if keep alive is enabled.
*/
private KeepAliveRunnable keepAliveRunnable;
/**
* Future for the currently scheduled {@link #writeTimeoutRunnable}.
*/
private ScheduledFuture> writeTimeoutFuture;
/**
* See {@link WriteTimeoutRunnable}.
* Initialized on startup if write timeout is enabled.
*/
private WriteTimeoutRunnable writeTimeoutRunnable;
/**
* See {@link ReaderCallable}.
* Initialized when a socket is opened.
*/
private Future> readerFuture;
/**
* When run, if the {@link AbstractLogstashTcpSocketAppender#keepAliveDuration}
* has elapsed since the last event was sent,
* then this runnable will publish a keepAlive event to the ringBuffer.
*
* The runnable will reschedule itself to execute in the future
* after the calculated {@link AbstractLogstashTcpSocketAppender#keepAliveDuration}
* from the last sent event using {@link TcpSendingEventHandler#scheduleKeepAlive(long)}.
*
* When the keepAlive event is processed by the event handler,
* if the {@link AbstractLogstashTcpSocketAppender#keepAliveDuration}
* has elapsed since the last event was sent,
* then the event handler will send the {@link AbstractLogstashTcpSocketAppender#keepAliveMessage}
* to the socket outputstream.
*
*/
private class KeepAliveRunnable implements Runnable {
private int previousDestinationIndex = connectedDestinationIndex;
@Override
public void run() {
long lastSendEnd = lastSendEndNanoTime;
long currentNanoTime = System.nanoTime();
if (hasKeepAliveDurationElapsed(lastSendEnd, currentNanoTime)) {
/*
* Publish a keep alive message to the RingBuffer.
*
* A null event indicates that this is a keep alive message.
*
* Use tryPublishEvent instead of publishEvent, because if the ring buffer is full,
* there's really no need to send a keep alive, since
* there are other messages waiting to be sent.
*/
getDisruptor().getRingBuffer().tryPublishEvent(getEventTranslator(), null);
scheduleKeepAlive(currentNanoTime);
} else {
scheduleKeepAlive(lastSendEnd);
}
if (previousDestinationIndex != connectedDestinationIndex) {
/*
* Destination has changed since last keep alive event,
* so update the thread name
*/
updateCurrentThreadName();
}
previousDestinationIndex = connectedDestinationIndex;
}
}
/**
* Keeps reading the {@link ReaderCallable#inputStream} until the
* end of the stream is reached.
*
* This helps pro-actively detect server-side socket disconnections,
* specifically in the case of Amazon's Elastic Load Balancers (ELB).
*/
private class ReaderCallable implements Callable {
private final InputStream inputStream;
public ReaderCallable(InputStream inputStream) {
super();
this.inputStream = inputStream;
}
@Override
public Void call() throws Exception {
updateCurrentThreadName();
try {
while (true) {
try {
if (inputStream.read() == -1) {
/*
* End of stream reached, so we're done.
*/
return null;
}
} catch (SocketTimeoutException e) {
/*
* ignore, and try again
*/
} catch (Exception e) {
/*
* Something else bad happened, so we're done.
*/
throw e;
}
}
} finally {
if (!Thread.currentThread().isInterrupted()) {
getExecutorService().submit(() -> {
/*
* https://github.com/logstash/logstash-logback-encoder/issues/341
*
* Pro-actively trigger the event handler's onEvent method in the handler thread
* by publishing a null event (which usually means a keepAlive event).
*
* When onEvent handles the event in the handler thread,
* it will detect that readerFuture.isDone() and reopen the socket.
*
* Without this, onEvent would not be called until the next event,
* which might not occur for a while.
* So, this is really just an optimization to reopen the socket as soon as possible.
*
* We can't reopen the socket from this thread,
* since all socket open/close must be done from the event handler thread.
*
* There is a potential race condition here as well, since
* onEvent could be triggered before the readerFuture completes.
* We reduce (but not eliminate) the chance of that happening by
* scheduling this task on the executorService.
*/
getDisruptor().getRingBuffer().tryPublishEvent(getEventTranslator(), null);
});
}
}
}
}
/**
* Detects write timeouts by inspecting {@link #lastSendStartNanoTime} and {@link #lastSendEndNanoTime}
*/
private class WriteTimeoutRunnable implements Runnable {
/**
* The lastSendStartNanoTime of the last detected timeout.
* Used to ensure we only detect a write timeout for a single write once
* (especially if the log rate is very low).
*/
private volatile long lastDetectedStartNanoTime;
@Override
public void run() {
long lastSendStart = lastSendStartNanoTime; // volatile read
long lastSendEnd = lastSendEndNanoTime; // volatile read
/*
* A write is in progress if the start is greater than the end
*/
if (lastSendStart > lastSendEnd && lastSendStart != lastDetectedStartNanoTime) {
long elapsedSendTimeInMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - lastSendStart);
if (elapsedSendTimeInMillis > writeTimeout.getMilliseconds()) {
lastDetectedStartNanoTime = lastSendStart;
addWarn(peerId + "Detected write timeout after " + elapsedSendTimeInMillis + "ms. Write timeout=" + getWriteTimeout() + ". Closing socket to force reconnect");
closeSocket();
}
}
}
}
@Override
public void onEvent(LogEvent logEvent, long sequence, boolean endOfBatch) throws Exception {
Exception sendFailureException = null;
for (int i = 0; i < MAX_REPEAT_WRITE_ATTEMPTS; i++) {
/*
* Save local references to the outputStream and socket
* in case the WriteTimeoutRunnable closes the socket.
*/
Socket socket = this.socket; // volatile read
OutputStream outputStream = this.outputStream; // volatile read
if (socket == null && (!isStarted() || Thread.currentThread().isInterrupted())) {
/*
* Handle shutdown in progress
*
* This will occur if shutdown occurred during reopen()
*/
sendFailureException = SHUTDOWN_IN_PROGRESS_EXCEPTION;
break;
}
Future> readerFuture = this.readerFuture; // volatile read
if (readerFuture.isDone() || socket == null) {
/*
* If readerFuture.isDone(), then the destination has shut down its output (our input),
* and the destination is probably no longer listening to its input (our output).
* This will be the case for Amazon's Elastic Load Balancers (ELB)
* when an instance behind the ELB becomes unhealthy while we're connected to it.
*
* If socket == null here, it means that a write timed out,
* and the socket was closed by the WriteTimeoutRunnable.
*
* Therefore, attempt reconnection.
*/
addInfo(peerId + "destination terminated the connection. Reconnecting.");
reopenSocket();
try {
readerFuture.get();
sendFailureException = NOT_CONNECTED_EXCEPTION;
} catch (Exception e) {
sendFailureException = e;
}
continue;
}
try {
writeEvent(socket, outputStream, logEvent, endOfBatch);
return;
} catch (Exception e) {
sendFailureException = e;
addWarn(peerId + "unable to send event: " + e.getMessage() + " Reconnecting.", e);
/*
* Need to re-open the socket in case of IOExceptions.
*
* Reopening the socket probably won't help other exceptions
* (like NullPointerExceptions),
* but we're doing so anyway, just in case.
*/
reopenSocket();
}
}
if (logEvent.event != null) {
fireEventSendFailure(logEvent.event, sendFailureException);
}
}
private void writeEvent(Socket socket, OutputStream outputStream, LogEvent logEvent, boolean endOfBatch) throws IOException {
long startWallTime = System.currentTimeMillis();
long startNanoTime = System.nanoTime();
lastSendStartNanoTime = startNanoTime;
/*
* A null event indicates that this is a keep alive message,
* or an event sent from the ReaderCallable.
*/
if (logEvent.event != null) {
/*
* This is a standard (non-keepAlive) event.
* Therefore, we need to send the event.
*/
if (getLogback11Support().isLogback11OrBefore()) {
getLogback11Support().doEncode(encoder, logEvent.event);
} else {
outputStream.write(encoder.encode(logEvent.event));
}
} else if (hasKeepAliveDurationElapsed(lastSendEndNanoTime, startNanoTime)){
/*
* This is a keep alive event, and the keepAliveDuration has passed,
* Therefore, we need to send the keepAliveMessage.
*/
outputStream.write(keepAliveBytes);
}
if (endOfBatch) {
outputStream.flush();
}
long endNanoTime = System.nanoTime();
lastSendEndNanoTime = endNanoTime;
if (logEvent.event != null) {
fireEventSent(socket, logEvent.event, endNanoTime - startNanoTime);
}
/*
* Should we close the current connection, and attempt to reconnect to another destination?
*/
if (connectionStrategy.shouldReconnect(startWallTime, connectedDestinationIndex, destinations.size())) {
addInfo(peerId + "reestablishing connection.");
outputStream.flush();
reopenSocket();
}
}
private boolean hasKeepAliveDurationElapsed(long lastSentNanoTime, long currentNanoTime) {
return isKeepAliveEnabled()
&& lastSentNanoTime + TimeUnit.MILLISECONDS.toNanos(keepAliveDuration.getMilliseconds()) < currentNanoTime;
}
@Override
public void onStart() {
this.destinationAttemptStartTimes = new long[destinations.size()];
openSocket();
scheduleKeepAlive(System.nanoTime());
scheduleWriteTimeout();
}
@Override
public void onShutdown() {
unscheduleWriteTimeout();
unscheduleKeepAlive();
closeEncoder();
closeSocket();
}
private synchronized void reopenSocket() {
closeSocket();
openSocket();
}
/**
* Repeatedly tries to open a socket until it is successful,
* or the hander is stopped, or the handler thread is interrupted.
*
* If the socket is non-null when this method returns,
* then it should be able to be used to send.
*/
private synchronized void openSocket() {
int errorCount = 0;
int destinationIndex = connectedDestinationIndex;
while (isStarted() && !Thread.currentThread().isInterrupted()) {
destinationIndex = connectionStrategy.selectNextDestinationIndex(destinationIndex, destinations.size());
long startWallTime = System.currentTimeMillis();
Socket tempSocket = null;
OutputStream tempOutputStream = null;
/*
* Choose next server
*/
InetSocketAddress currentDestination = destinations.get(destinationIndex);
try {
/*
* Update peerId (for status message)
*/
peerId = "Log destination " + currentDestination + ": ";
/*
* Delay the connection attempt if the last attempt to the selected destination
* was less than the reconnectionDelay.
*/
final long millisSinceLastAttempt = startWallTime - destinationAttemptStartTimes[destinationIndex];
if (millisSinceLastAttempt < reconnectionDelay.getMilliseconds()) {
final long sleepTime = reconnectionDelay.getMilliseconds() - millisSinceLastAttempt;
if (errorCount < MAX_REPEAT_CONNECTION_ERROR_LOG * destinations.size()) {
addWarn(peerId + "Waiting " + sleepTime + "ms before attempting reconnection.");
}
try {
shutdownLatch.await(sleepTime, TimeUnit.MILLISECONDS);
if (!isStarted()) {
return;
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
addWarn(peerId + "connection interrupted. Will no longer attempt reconnection.");
return;
}
// reset the start time to be after the wait period.
startWallTime = System.currentTimeMillis();
}
destinationAttemptStartTimes[destinationIndex] = startWallTime;
/*
* Set the SO_TIMEOUT so that SSL handshakes will timeout if they take too long.
*
* Note that SO_TIMEOUT only applies to reads (which occur during the handshake process).
*/
tempSocket = socketFactory.createSocket();
tempSocket.setSoTimeout(acceptConnectionTimeout);
/*
* currentDestination is unresolved, so a new InetSocketAddress
* must be created to resolve the hostname.
*/
tempSocket.connect(new InetSocketAddress(getHostString(currentDestination), currentDestination.getPort()), acceptConnectionTimeout);
/*
* Trigger SSL handshake immediately and declare the socket unconnected if it fails
*/
if (tempSocket instanceof SSLSocket) {
((SSLSocket)tempSocket).startHandshake();
}
/*
* Issue #218, make buffering the output stream optional.
*/
tempOutputStream = writeBufferSize > 0
? new BufferedOutputStream(tempSocket.getOutputStream(), writeBufferSize)
: tempSocket.getOutputStream();
if (getLogback11Support().isLogback11OrBefore()) {
getLogback11Support().init(encoder, tempOutputStream);
}
addInfo(peerId + "connection established.");
this.socket = tempSocket;
this.outputStream = tempOutputStream;
boolean shouldUpdateThreadName = (destinationIndex != connectedDestinationIndex);
connectedDestinationIndex = destinationIndex;
connectedDestination = currentDestination;
connectionStrategy.connectSuccess(startWallTime, destinationIndex, destinations.size());
if (shouldUpdateThreadName) {
/*
* destination has changed, so update the thread name
*/
updateCurrentThreadName();
}
this.readerFuture = scheduleReaderCallable(
new ReaderCallable(tempSocket.getInputStream()));
fireConnectionOpened(this.socket);
return;
} catch (Exception e) {
CloseUtil.closeQuietly(tempOutputStream);
CloseUtil.closeQuietly(tempSocket);
connectionStrategy.connectFailed(startWallTime, destinationIndex, destinations.size());
fireConnectionFailed(currentDestination, e);
/*
* Avoid spamming status messages by checking the MAX_REPEAT_CONNECTION_ERROR_LOG.
*/
if (errorCount++ < MAX_REPEAT_CONNECTION_ERROR_LOG * destinations.size()) {
addWarn(peerId + "connection failed.", e);
}
}
}
}
private synchronized void closeSocket() {
connectedDestination = null;
CloseUtil.closeQuietly(outputStream);
outputStream = null;
CloseUtil.closeQuietly(socket);
fireConnectionClosed(socket);
socket = null;
if (this.readerFuture != null) {
/*
* This shouldn't be necessary, since closing the socket
* should cause the read() call to throw an exception.
*
* But cancel it anyway to be extra-safe.
*/
this.readerFuture.cancel(true);
}
}
private void closeEncoder() {
if (getLogback11Support().isLogback11OrBefore()) {
try {
getLogback11Support().close(encoder);
} catch (IOException ioe) {
addError("Failed to close encoder", ioe);
}
}
encoder.stop();
}
private synchronized void scheduleKeepAlive(long basedOnNanoTime) {
if (isKeepAliveEnabled() && !Thread.currentThread().isInterrupted()) {
if (keepAliveRunnable == null) {
keepAliveRunnable = new KeepAliveRunnable();
}
long delay = TimeUnit.MILLISECONDS.toNanos(keepAliveDuration.getMilliseconds()) - (System.nanoTime() - basedOnNanoTime);
try {
keepAliveFuture = getExecutorService().schedule(
keepAliveRunnable,
delay,
TimeUnit.NANOSECONDS);
} catch (RejectedExecutionException e) {
/*
* if scheduling failed, it means that the appender is shutting down.
*/
keepAliveFuture = null;
}
}
}
private synchronized void unscheduleKeepAlive() {
if (keepAliveFuture != null) {
keepAliveFuture.cancel(true);
try {
keepAliveFuture.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ignore
} catch (Exception e) {
// ignore
}
}
}
private synchronized void scheduleWriteTimeout() {
if (isWriteTimeoutEnabled() && !Thread.currentThread().isInterrupted()) {
if (writeTimeoutRunnable == null) {
writeTimeoutRunnable = new WriteTimeoutRunnable();
}
long delay = writeTimeout.getMilliseconds();
try {
writeTimeoutFuture = getExecutorService().scheduleWithFixedDelay(
writeTimeoutRunnable,
delay,
delay,
TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
/*
* if scheduling failed, it means that the appender is shutting down.
*/
writeTimeoutFuture = null;
}
}
}
private synchronized void unscheduleWriteTimeout() {
if (writeTimeoutFuture != null) {
writeTimeoutFuture.cancel(true);
try {
writeTimeoutFuture.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ignore
} catch (Exception e) {
// ignore
}
}
}
}
/**
* An extension of logback's {@link ConfigurableSSLSocketFactory}
* that supports creating unconnected sockets
* (via {@link UnconnectedConfigurableSSLSocketFactory#createSocket()})
* so that a custom connection timeout can be used when connecting.
*/
private static class UnconnectedConfigurableSSLSocketFactory extends ConfigurableSSLSocketFactory {
private final SSLParametersConfiguration parameters;
private final SSLSocketFactory delegate;
public UnconnectedConfigurableSSLSocketFactory(SSLParametersConfiguration parameters, SSLSocketFactory delegate) {
super(parameters, delegate);
this.parameters = parameters;
this.delegate = delegate;
}
@Override
public Socket createSocket() throws IOException {
SSLSocket socket = (SSLSocket) delegate.createSocket();
parameters.configure(new SSLConfigurableSocket(socket));
return socket;
}
}
public AbstractLogstashTcpSocketAppender() {
super();
setEventHandler(new TcpSendingEventHandler());
setThreadNameFormat(DEFAULT_THREAD_NAME_FORMAT);
}
@Override
public boolean isStarted() {
CountDownLatch latch = this.shutdownLatch;
return latch != null && latch.getCount() != 0;
}
@Override
public synchronized void start() {
if (isStarted()) {
return;
}
int errorCount = 0;
if (encoder == null) {
errorCount++;
addError("No encoder was configured. Use to specify the fully qualified class name of the encoder to use");
}
/*
* Destinations can be configured via / OR but not both!
*/
if (!destinations.isEmpty() && remoteHost != null) {
errorCount++;
addError("Use '/' or '' but not both");
}
/*
* Handle destination specified using /
*/
if (remoteHost != null) {
addWarn("/ are DEPRECATED, use instead");
try {
addDestinations(InetSocketAddress.createUnresolved(remoteHost, port));
}
catch (IllegalArgumentException e) {
errorCount++;
addError(e.getMessage());
}
}
/*
* Make sure at least one destination has been specified
*/
if (destinations.isEmpty()) {
errorCount++;
addError("No destination was configured. Use to add one or more destinations to the appender");
}
/*
* Create socket factory
*/
if (errorCount == 0 && socketFactory == null) {
if (sslConfiguration == null) {
socketFactory = SocketFactory.getDefault();
} else {
try {
SSLContext sslContext = getSsl().createContext(this);
SSLParametersConfiguration parameters = getSsl().getParameters();
parameters.setContext(getContext());
socketFactory = new UnconnectedConfigurableSSLSocketFactory(
parameters,
sslContext.getSocketFactory());
} catch (Exception e) {
addError("Unable to create ssl context", e);
errorCount++;
}
}
}
if (keepAliveMessage != null && keepAliveCharset != null) {
keepAliveBytes = keepAliveMessage.getBytes(keepAliveCharset);
}
if (errorCount == 0) {
encoder.setContext(getContext());
if (!encoder.isStarted()) {
encoder.start();
}
/*
* Increase the core size to handle the reader thread
*/
int threadPoolCoreSize = getThreadPoolCoreSize() + 1;
/*
* Increase the core size to handle the keep alive thread
*/
if (keepAliveDuration != null) {
threadPoolCoreSize++;
}
/*
* Increase the core size to handle the write timeout detection thread
*/
if (isWriteTimeoutEnabled()) {
threadPoolCoreSize++;
}
setThreadPoolCoreSize(threadPoolCoreSize);
this.shutdownLatch = new CountDownLatch(1);
super.start();
}
}
@Override
public synchronized void stop() {
if (!isStarted()) {
return;
}
/*
* Stop waiting to reconnect (if reconnect logic is currently waiting)
*/
this.shutdownLatch.countDown();
super.stop();
}
protected Future> scheduleReaderCallable(Callable readerCallable) {
return getExecutorService().submit(readerCallable);
}
protected void fireEventSent(Socket socket, Event event, long durationInNanos) {
for (Listener listener : listeners) {
listener.eventSent(this, socket, event, durationInNanos);
}
}
protected void fireEventSendFailure(Event event, Throwable reason) {
for (Listener listener : listeners) {
listener.eventSendFailure(this, event, reason);
}
}
protected void fireConnectionOpened(Socket socket) {
for (Listener listener : listeners) {
listener.connectionOpened(this, socket);
}
}
protected void fireConnectionClosed(Socket socket) {
for (Listener listener : listeners) {
listener.connectionClosed(this, socket);
}
}
protected void fireConnectionFailed(InetSocketAddress address, Throwable throwable) {
for (Listener listener : listeners) {
listener.connectionFailed(this, address, throwable);
}
}
/*
* Visible for unit testing
*/
protected Logback11Support getLogback11Support() {
return Logback11Support.INSTANCE;
}
public Encoder getEncoder() {
return encoder;
}
public void setEncoder(Encoder encoder) {
this.encoder = encoder;
}
public SocketFactory getSocketFactory() {
return socketFactory;
}
/**
* Used to create client {@link Socket}s to which to communicate.
* By default, it is the system default SocketFactory.
*/
public void setSocketFactory(SocketFactory socketFactory) {
this.socketFactory = socketFactory;
}
/**
* The host to which to connect and send events
*
* @deprecated use {@link #addDestination(String)} instead
*/
@Deprecated
public void setRemoteHost(String host) {
remoteHost = host;
}
@Deprecated
public String getRemoteHost() {
return remoteHost;
}
/**
* The TCP port on the host to which to connect and send events
*
* @deprecated use {@link #addDestination(String)} instead
*/
@Deprecated
public void setPort(int port) {
this.port = port;
}
@Deprecated
public int getPort() {
return port;
}
/**
* Adds the given destination (or destinations) to the list of potential destinations
* to which to send logs.
*
*
* The string is a comma separated list of destinations in the form of hostName[:portNumber].
*
* If portNumber is not provided, then the configured {@link #port} will be used,
* which defaults to {@value #DEFAULT_PORT}
*
*
* For example, "host1.domain.com,host2.domain.com:5560"
*/
public void addDestination(final String destination) throws IllegalArgumentException {
List parsedDestinations = DestinationParser.parse(destination, DEFAULT_PORT);
addDestinations(parsedDestinations.toArray(new InetSocketAddress[parsedDestinations.size()]));
}
/**
* Adds the given destinations to the list of potential destinations.
*/
public void addDestinations(InetSocketAddress... destinations) throws IllegalArgumentException {
if (destinations == null) {
return;
}
for (InetSocketAddress destination : destinations) {
try {
InetAddress.getByName(getHostString(destination));
}
catch (UnknownHostException ex) {
/*
* Warn, but don't fail startup, so that transient
* DNS problems are allowed to resolve themselves eventually.
*/
addWarn("Invalid destination '" + getHostString(destination) + "': host unknown (was '" + getHostString(destination) + "').");
}
this.destinations.add(destination);
}
}
/**
* Returns the host string from the given destination,
* avoiding a DNS hit if possible.
*/
protected String getHostString(InetSocketAddress destination) {
/*
* Avoid the potential DNS hit by using getHostString() instead of getHostName()
*/
return destination.getHostString();
}
protected void updateCurrentThreadName() {
Thread.currentThread().setName(calculateThreadName());
}
@Override
protected List