All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.logstash.logback.appender.AbstractLogstashTcpSocketAppender Maven / Gradle / Ivy

Go to download

Provides logback encoders, layouts, and appenders to log in JSON and other formats supported by Jackson

There is a newer version: 8.0
Show newest version
/**
 * 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 getThreadNameFormatParams() { List superThreadNameFormatParams = super.getThreadNameFormatParams(); List threadNameFormatParams = new ArrayList(superThreadNameFormatParams.size() + 2); threadNameFormatParams.addAll(superThreadNameFormatParams); InetSocketAddress currentDestination = this.destinations.get(connectedDestinationIndex); threadNameFormatParams.add(getHostString(currentDestination)); threadNameFormatParams.add(currentDestination.getPort()); return threadNameFormatParams; } /** * Return the destinations in which to attempt to send logs. */ public List getDestinations() { return Collections.unmodifiableList(destinations); } /** * Time period for which to wait after failing to connect to all servers, * before attempting to reconnect. * Default is {@value #DEFAULT_RECONNECTION_DELAY} milliseconds. */ public void setReconnectionDelay(Duration delay) { if (delay == null || delay.getMilliseconds() <= 0) { throw new IllegalArgumentException("reconnectionDelay must be > 0"); } this.reconnectionDelay = delay; } public Duration getReconnectionDelay() { return reconnectionDelay; } /** * Convenience method for setting {@link PreferPrimaryDestinationConnectionStrategy#setSecondaryConnectionTTL(Duration)}. * * When the {@link #connectionStrategy} is a {@link PreferPrimaryDestinationConnectionStrategy}, * this will set its {@link PreferPrimaryDestinationConnectionStrategy#setSecondaryConnectionTTL(Duration)}. * * @see PreferPrimaryDestinationConnectionStrategy#setSecondaryConnectionTTL(Duration) * @throws IllegalStateException if the {@link #connectionStrategy} is not a {@link PreferPrimaryDestinationConnectionStrategy} */ public void setSecondaryConnectionTTL(Duration secondaryConnectionTTL) { if (connectionStrategy instanceof PreferPrimaryDestinationConnectionStrategy) { ((PreferPrimaryDestinationConnectionStrategy) connectionStrategy).setSecondaryConnectionTTL(secondaryConnectionTTL); } else { throw new IllegalStateException(String.format("When setting the secondaryConnectionTTL, the strategy must be a %s. It is currently a %s", PreferPrimaryDestinationConnectionStrategy.class, connectionStrategy)); } } public Duration getSecondaryConnectionTTL() { if (connectionStrategy instanceof PreferPrimaryDestinationConnectionStrategy) { return ((PreferPrimaryDestinationConnectionStrategy) connectionStrategy).getSecondaryConnectionTTL(); } return null; } /** * Socket connection timeout in milliseconds. */ void setAcceptConnectionTimeout(int acceptConnectionTimeout) { this.acceptConnectionTimeout = acceptConnectionTimeout; } public int getWriteBufferSize() { return writeBufferSize; } /** * 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. */ public void setWriteBufferSize(int writeBufferSize) { this.writeBufferSize = writeBufferSize; } /** * Returns the maximum number of events in the queue. */ public int getQueueSize() { return getRingBufferSize(); } /** * Sets the maximum number of events in the queue. Once the queue is full * additional events will be dropped. * *

* Must be a positive power of 2. * * @param queueSize the maximum number of entries in the queue. */ public void setQueueSize(int queueSize) { setRingBufferSize(queueSize); } public SSLConfiguration getSsl() { return sslConfiguration; } /** * Set this to non-null to use SSL. * See the logback manual * for details on how to configure SSL for a client. */ public void setSsl(SSLConfiguration sslConfiguration) { this.sslConfiguration = sslConfiguration; } public Duration getKeepAliveDuration() { return keepAliveDuration; } /** * 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, no keepAlive messages will be sent. */ public void setKeepAliveDuration(Duration keepAliveDuration) { this.keepAliveDuration = keepAliveDuration; } public String getKeepAliveMessage() { return keepAliveMessage; } /** * Message to send for keeping the connection alive * if {@link #keepAliveDuration} is non-null. * * The following values have special meaning: *

    *
  • null or empty string = no keep alive.
  • *
  • "SYSTEM" = operating system new line (default).
  • *
  • "UNIX" = unix line ending (\n).
  • *
  • "WINDOWS" = windows line ending (\r\n).
  • *
*

* Any other value will be used as-is. */ public void setKeepAliveMessage(String keepAliveMessage) { this.keepAliveMessage = SeparatorParser.parseSeparator(keepAliveMessage); } public boolean isKeepAliveEnabled() { return this.keepAliveDuration != null && this.keepAliveMessage != null; } public boolean isWriteTimeoutEnabled() { return this.writeTimeout.getMilliseconds() > 0; } public Charset getKeepAliveCharset() { return keepAliveCharset; } /** * The charset to use when writing the {@link #keepAliveMessage}. * Defaults to UTF-8. */ public void setKeepAliveCharset(Charset keepAliveCharset) { this.keepAliveCharset = keepAliveCharset; } /** * Pattern used by the to set the handler thread name. * Defaults to {@value #DEFAULT_THREAD_NAME_FORMAT}. *

* * If you change the {@link #threadFactory}, then this * value may not be honored. *

* * The string is a format pattern understood by {@link Formatter#format(String, Object...)}. * {@link Formatter#format(String, Object...)} is used to * construct the actual thread name prefix. * The first argument (%1$s) is the string appender name. * The second argument (%2$d) is the numerical thread index. * The third argument (%3$s) is the string hostname of the currently connected destination. * The fourth argument (%4$d) is the numerical port of the currently connected destination. * Other arguments can be made available by subclasses. */ @Override public void setThreadNameFormat(String threadNameFormat) { super.setThreadNameFormat(threadNameFormat); } public DestinationConnectionStrategy getConnectionStrategy() { return connectionStrategy; } @DefaultClass(DelegateDestinationConnectionStrategy.class) public void setConnectionStrategy(DestinationConnectionStrategy destinationConnectionStrategy) { this.connectionStrategy = destinationConnectionStrategy; } /** * Returns the currently connected destination as an {@link Optional}. * The {@link Optional} will be absent if the appender is not currently connected. * * @return the currently connected destination as an {@link Optional}. * The {@link Optional} will be absent if the appender is not currently connected. */ public Optional getConnectedDestination() { return Optional.ofNullable(this.connectedDestination); } public Duration getWriteTimeout() { return writeTimeout; } /** * Sets the 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.

*/ public void setWriteTimeout(Duration writeTimeout) { this.writeTimeout = writeTimeout == null ? new Duration(DEFAULT_WRITE_TIMEOUT) : writeTimeout; } }