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

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

There is a newer version: 8.0
Show newest version
/*
 * Copyright 2013-2022 the original author or authors.
 *
 * 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.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
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.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.CompositeJsonEncoder;
import net.logstash.logback.encoder.SeparatorParser;
import net.logstash.logback.encoder.StreamingEncoder;
import net.logstash.logback.util.ReusableByteBuffer;

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 net.logstash.logback.encoder.com.lmax.disruptor.EventHandler;
import net.logstash.logback.encoder.com.lmax.disruptor.LifecycleAware;
import net.logstash.logback.encoder.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 * @deprecated Use {@link #DEFAULT_RING_BUFFER_SIZE} instead */ @Deprecated public static final int DEFAULT_QUEUE_SIZE = DEFAULT_RING_BUFFER_SIZE; /** * Default timeout when waiting for the remote server to accept our * connection. The same timeout is used as a read timeout during SSL * handshake. */ 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)}); } /** * 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. * Must be positive. A value of zero is interpreted as an infinite timeout. */ private Duration connectionTimeout = new Duration(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 = SeparatorParser.parseSeparator("UNIX"); /** * 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. */ 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; /** * The {@link ScheduledExecutorService} used to execute house keeping tasks. */ private ScheduledThreadPoolExecutor executorService; /** * 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; /** * 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; /** * Intermediate ByteBuffer used to store content generated by {@link StreamingEncoder}. * Set when {@link #onStart()} but stays uninitialized if encoder is a "raw" {@link Encoder}. */ private ReusableByteBuffer buffer; /** * 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; 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()) { executorService.submit(() -> /* * https://github.com/logfellow/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 (writeTimeout=" + getWriteTimeout() + "). Closing socket to force reconnect."); closeSocket(); } } } } @Override public void onEvent(LogEvent logEvent, long sequence, boolean endOfBatch) throws Exception { while (true) { /* * 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() */ fireEventSendFailure(logEvent.event, SHUTDOWN_IN_PROGRESS_EXCEPTION); break; } /* * If socket == null here, it means that a write timed out, * and the socket was closed by the WriteTimeoutRunnable. * * Note: a warning status has already been emitted by WriteTimeoutRunnable, * no need to repeat here. */ if (socket == null) { reopenSocket(); continue; } /* * 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. */ Future readerFuture = this.readerFuture; // volatile read if (readerFuture.isDone()) { String msg = "destination terminated the connection"; try { readerFuture.get(); } catch (ExecutionException e) { msg += " (cause: " + e.getCause().getMessage() + ")"; } addInfo(peerId + msg + ". Reconnecting."); reopenSocket(); continue; } /* * Write the event in the output stream. * Drop event if encoder throws an exception. * Reconnect if an exception is thrown by the connection stream itself. */ try { writeEvent(socket, outputStream, logEvent, endOfBatch); return; } catch (EncoderException e) { /* * Encoding threw an exception. Warn and drop event before it becomes a "poison". */ addWarn(peerId + "Encoder failed to encode event. Dropping event.", e.getCause()); fireEventSendFailure(logEvent.event, e.getCause()); break; } catch (Exception e) { /* * Any other exception is thrown by the socket stream (or bug in the code). * Re-open the socket and get a fresh new stream. */ addWarn(peerId + "Unable to send event. Reconnecting.", e); reopenSocket(); } } } private void writeEvent(Socket socket, OutputStream outputStream, LogEvent logEvent, boolean endOfBatch) throws IOException, EncoderException { 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. */ encode(logEvent.event, outputStream); } 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(); } } @SuppressWarnings("unchecked") private void encode(Event event, OutputStream outputStream) throws IOException, EncoderException { if (encoder instanceof StreamingEncoder) { /* * Use a temporary buffer to avoid writing "partial" content in the output * stream if the Encoder throws an exception. */ try { try { ((StreamingEncoder) encoder).encode(event, buffer); } catch (Exception e) { throw new EncoderException(e); } buffer.writeTo(outputStream); } finally { buffer.reset(); } } else { byte[] data; try { data = encoder.encode(event); } catch (Exception e) { throw new EncoderException(e); } if (data != null) { outputStream.write(data); } } } 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()]; if (encoder instanceof CompositeJsonEncoder) { this.buffer = new ReusableByteBuffer(((CompositeJsonEncoder) encoder).getMinBufferSize()); } else if (encoder instanceof StreamingEncoder) { this.buffer = new ReusableByteBuffer(); } 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((int) connectionTimeout.getMilliseconds()); /* * currentDestination is unresolved, so a new InetSocketAddress * must be created to resolve the hostname. */ tempSocket.connect(new InetSocketAddress(getHostString(currentDestination), currentDestination.getPort()), (int) connectionTimeout.getMilliseconds()); /* * 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(); 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() { encoder.stop(); buffer = null; } 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 = executorService.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 = executorService.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 } } } } /** * Wrap exceptions thrown by {@link Encoder} */ @SuppressWarnings("serial") private static class EncoderException extends Exception { EncoderException(Throwable cause) { super(cause); } } /** * 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; 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(); setThreadNameFormat(DEFAULT_THREAD_NAME_FORMAT); } @Override protected EventHandler> createEventHandler() { return new TcpSendingEventHandler(); } @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"); } /* * 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) { keepAliveBytes = keepAliveMessage.getBytes(keepAliveCharset); } if (errorCount == 0) { encoder.setContext(getContext()); if (!encoder.isStarted()) { encoder.start(); } /* * Start with an initial core size of 1 to handle the Reader thread */ int threadPoolCoreSize = 1; /* * Increase the core size to handle the keep alive thread */ if (isKeepAliveEnabled()) { threadPoolCoreSize++; } /* * Increase the core size to handle the write timeout detection thread */ if (isWriteTimeoutEnabled()) { threadPoolCoreSize++; } this.executorService = new ScheduledThreadPoolExecutor( threadPoolCoreSize, getThreadFactory()); /* * This ensures that cancelled tasks do not hold up shutdown. */ this.executorService.setRemoveOnCancelPolicy(true); this.shutdownLatch = new CountDownLatch(1); super.start(); } } @Override public synchronized void stop() { if (!isStarted()) { return; } super.stop(); /* * Stop waiting to reconnect (if reconnect logic is currently waiting) */ this.shutdownLatch.countDown(); /* * Stop executor service */ this.executorService.shutdown(); try { if (!this.executorService.awaitTermination(1, TimeUnit.MINUTES)) { addWarn("Some queued events have not been logged due to requested shutdown"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); addWarn("Some queued events have not been logged due to requested shutdown", e); } } protected Future scheduleReaderCallable(Callable readerCallable) { return executorService.submit(readerCallable); } protected void fireEventSent(Socket socket, Event event, long durationInNanos) { if (event != null) { safelyFireEvent(l -> l.eventSent(this, socket, event, durationInNanos)); } } protected void fireEventSendFailure(Event event, Throwable reason) { if (event != null) { safelyFireEvent(l -> l.eventSendFailure(this, event, reason)); } } protected void fireConnectionOpened(Socket socket) { safelyFireEvent(l -> l.connectionOpened(this, socket)); } protected void fireConnectionClosed(Socket socket) { safelyFireEvent(l -> l.connectionClosed(this, socket)); } protected void fireConnectionFailed(InetSocketAddress address, Throwable throwable) { safelyFireEvent(l -> l.connectionFailed(this, address, throwable)); } public Encoder getEncoder() { return encoder; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } public SocketFactory getSocketFactory() { return socketFactory; } /** * Set the {@link SocketFactory} used to create client {@link Socket}s to which to communicate. * Use {@code null} to use the system default SocketFactory. * * @param socketFactory the socket factory to use to create connections with remote destinations. */ public void setSocketFactory(SocketFactory socketFactory) { this.socketFactory = socketFactory; } /** * 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 default ({@value #DEFAULT_PORT}) will be used *

* * For example, "host1.domain.com,host2.domain.com:5560" * * @param destination comma-separated list of destinations in the form of {@code hostName[:portNumber]} */ public void addDestination(final String destination) throws IllegalArgumentException { List parsedDestinations = DestinationParser.parse(destination, DEFAULT_PORT); addDestinations(parsedDestinations.toArray(new InetSocketAddress[0])); } /** * Adds the given destinations to the list of potential destinations. * * @param destinations the {@link InetSocketAddress} to add to the list of valid 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. * * @param destination the {@link InetSocketAddress} to get the host string from * @return the host string of the given destination */ 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. * * @return an ordered list of {@link InetSocketAddress} representing the configured destinations */ 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. * * @param delay the reconnection delay */ 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) * @param secondaryConnectionTTL the TTL of a connection when connected to a secondary destination * @throws IllegalStateException if the {@link #connectionStrategy} is not a {@link PreferPrimaryDestinationConnectionStrategy} * * @deprecated use {@link PreferPrimaryDestinationConnectionStrategy#setSecondaryConnectionTTL(Duration)} instead. */ @Deprecated public void setSecondaryConnectionTTL(Duration secondaryConnectionTTL) { addWarn( "Setting directly on the appender is deprecated. " + "Instead you should explicitly set the connection strategy to and set its property to the desired value."); 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)); } } /** * Convenience method for accessing {@link PreferPrimaryDestinationConnectionStrategy#getSecondaryConnectionTTL()}. * * @return the secondary connection TTL or {@code null} if the connection strategy is not a {@link PreferPrimaryDestinationConnectionStrategy}. * @deprecated use {@link PreferPrimaryDestinationConnectionStrategy#getSecondaryConnectionTTL()} instead. * * @see #getConnectionStrategy() * @see PreferPrimaryDestinationConnectionStrategy#getSecondaryConnectionTTL() */ @Deprecated public Duration getSecondaryConnectionTTL() { if (connectionStrategy instanceof PreferPrimaryDestinationConnectionStrategy) { return ((PreferPrimaryDestinationConnectionStrategy) connectionStrategy).getSecondaryConnectionTTL(); } return null; } /** * Set the connection timeout when establishing a connection to a remote destination. * * Use {@code 0} for an "infinite timeout" which often really means "use the OS defaults". * * @param connectionTimeout connection timeout */ public void setConnectionTimeout(Duration connectionTimeout) { if (Objects.requireNonNull(connectionTimeout).getMilliseconds() < 0) { throw new IllegalArgumentException("connectionTimeout must be a positive value"); } this.connectionTimeout = connectionTimeout; } /** * Get the connection timeout used when establishing a TCP connection to a remote destination. * * @return the connection timeout (never null). */ public Duration getConnectionTimeout() { return connectionTimeout; } 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. * * @param writeBufferSize the write buffer size in bytes */ public void setWriteBufferSize(int writeBufferSize) { this.writeBufferSize = writeBufferSize; } /** * Returns the maximum number of events in the queue. * Alias for {@link #getRingBufferSize()}. * * @return the size of the ring buffer * @deprecated use {@link #getRingBufferSize()} instead. */ @Deprecated 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. * @deprecated use {@link #setRingBufferSize(int)} instead. */ @Deprecated public void setQueueSize(int queueSize) { addWarn(" is deprecated, use instead"); 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. * * @param sslConfiguration the SSL configuration */ 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 {@code null}, zero or negative, no keepAlive messages will be sent. * * @param keepAliveDuration duration between consecutive keep alive messages */ 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 and strictly positive. * * The following values have special meaning: *

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

* Any other value will be used as-is. * * @param keepAliveMessage the keep alive message */ public void setKeepAliveMessage(String keepAliveMessage) { this.keepAliveMessage = SeparatorParser.parseSeparator(keepAliveMessage); } public boolean isKeepAliveEnabled() { return this.keepAliveDuration != null && this.keepAliveDuration.getMilliseconds() > 0 && this.keepAliveMessage != null && !this.keepAliveMessage.isEmpty(); } /** * Whether the write timeout feature is enabled or not. * * @return {@code true} when the appender should try to detect write timeouts, {@code false} otherwise. */ 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. * * @param keepAliveCharset charset encoding for the keep alive message */ public void setKeepAliveCharset(Charset keepAliveCharset) { this.keepAliveCharset = Objects.requireNonNull(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. * * @param threadNameFormat thread name format pattern */ @Override public void setThreadNameFormat(String threadNameFormat) { super.setThreadNameFormat(threadNameFormat); } public DestinationConnectionStrategy getConnectionStrategy() { return connectionStrategy; } @DefaultClass(DelegateDestinationConnectionStrategy.class) public void setConnectionStrategy(DestinationConnectionStrategy destinationConnectionStrategy) { this.connectionStrategy = Objects.requireNonNull(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. * This timeout is used to detect connections where the receiver stops reading. * *

The timeout must be > 0. A timeout of zero is interpreted as an infinite timeout * which effectively means "no write timeout". * *

Note that since a blocking java socket output stream does not have a concept * of a write timeout, a task will be scheduled 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. * * @param writeTimeout the write timeout */ public void setWriteTimeout(Duration writeTimeout) { if (writeTimeout == null || writeTimeout.getMilliseconds() < 0) { throw new IllegalArgumentException("writeTimeout must be >= 0"); } this.writeTimeout = writeTimeout; } }