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

org.logstash.beats.BeatsHandler Maven / Gradle / Ivy

The newest version!
package org.logstash.beats;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.wavefront.common.Utils;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.MetricName;

import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import javax.net.ssl.SSLHandshakeException;

@ChannelHandler.Sharable
public class BeatsHandler extends SimpleChannelInboundHandler {
    private final static Logger logger = LogManager.getLogger(BeatsHandler.class);
    private final IMessageListener messageListener;
    private final Supplier duplicateBatchesIgnored = Utils.lazySupplier(() ->
        Metrics.newCounter(new MetricName("logsharvesting", "", "filebeat-duplicate-batches")));
    private final Cache batchDedupeCache = Caffeine.newBuilder().
        expireAfterAccess(1, TimeUnit.HOURS).
        build();

    public BeatsHandler(IMessageListener listener) {
        messageListener = listener;
    }

    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        if (logger.isTraceEnabled()){
            logger.trace(format(ctx, "Channel Active"));
        }
        super.channelActive(ctx);
        messageListener.onNewConnection(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        if (logger.isTraceEnabled()){
            logger.trace(format(ctx, "Channel Inactive"));
        }
        messageListener.onConnectionClose(ctx);
    }


    @Override
    public void channelRead0(ChannelHandlerContext ctx, Batch batch) throws Exception {
        if(logger.isDebugEnabled()) {
            logger.debug(format(ctx, "Received a new payload"));
        }
        try {
            boolean isFirstMessage = true;
            String key;
            BatchIdentity value;
            for (Message message : batch) {
              if (isFirstMessage) {
                // check whether we've processed that batch already
                isFirstMessage = false;
                key = BatchIdentity.keyFrom(message);
                value = BatchIdentity.valueFrom(message);
                if (key != null && value != null) {
                  BatchIdentity cached = batchDedupeCache.getIfPresent(key);
                  if (value.equals(cached)) {
                    duplicateBatchesIgnored.get().inc();
                    if (logger.isDebugEnabled()) {
                      logger.debug(format(ctx, "Duplicate filebeat batch received, ignoring"));
                    }
                    // ack the entire batch and stop processing the rest of it
                    writeAck(ctx, message.getBatch().getProtocol(),
                        message.getBatch().getHighestSequence());
                    break;
                  } else {
                    batchDedupeCache.put(key, value);
                  }
                }
              }
              if (logger.isDebugEnabled()) {
                logger.debug(format(ctx, "Sending a new message for the listener, sequence: " +
                    message.getSequence()));
              }
              messageListener.onNewMessage(ctx, message);

              if (needAck(message)) {
                ack(ctx, message);
              }
            }
        }finally{
            //this channel is done processing this payload, instruct the connection handler to stop sending TCP keep alive
            ctx.channel().attr(ConnectionHandler.CHANNEL_SEND_KEEP_ALIVE).get().set(false);
            if (logger.isDebugEnabled()) {
                logger.debug("{}: batches pending: {}", ctx.channel().id().asShortText(),ctx.channel().attr(ConnectionHandler.CHANNEL_SEND_KEEP_ALIVE).get().get());
            }
            batch.release();
            ctx.flush();
        }
    }

    /*
     * Do not propagate the SSL handshake exception down to the ruby layer handle it locally instead and close the connection
     * if the channel is still active. Calling `onException` will flush the content of the codec's buffer, this call
     * may block the thread in the event loop until completion, this should only affect LS 5 because it still supports
     * the multiline codec, v6 drop support for buffering codec in the beats input.
     *
     * For v5, I cannot drop the content of the buffer because this will create data loss because multiline content can
     * overlap Filebeat transmission; we were recommending multiline at the source in v5 and in v6 we enforce it.
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        try {
            if (!(cause instanceof SSLHandshakeException)) {
                messageListener.onException(ctx, cause);
            }
            String causeMessage = cause.getMessage() == null ? cause.getClass().toString() : cause.getMessage();

            if (logger.isDebugEnabled()){
                logger.debug(format(ctx, "Handling exception: " + causeMessage), cause);
            }
            logger.info(format(ctx, "Handling exception: " + causeMessage));
        } finally{
            super.exceptionCaught(ctx, cause);
            ctx.flush();
            ctx.close();
        }
    }

    private boolean needAck(Message message) {
        return message.getSequence() == message.getBatch().getHighestSequence();
    }

    private void ack(ChannelHandlerContext ctx, Message message) {
        if (logger.isTraceEnabled()){
            logger.trace(format(ctx, "Acking message number " + message.getSequence()));
        }
        writeAck(ctx, message.getBatch().getProtocol(), message.getSequence());
        writeAck(ctx, message.getBatch().getProtocol(), 0); // send blank ack
    }

    private void writeAck(ChannelHandlerContext ctx, byte protocol, int sequence) {
        ctx.writeAndFlush(new Ack(protocol, sequence)).
            addListener((ChannelFutureListener) channelFuture -> {
                if (channelFuture.isSuccess() && logger.isTraceEnabled() && sequence > 0) {
                    logger.trace(format(ctx, "Ack complete for message number " + sequence));
                }
            });
    }

    /*
     * There is no easy way in Netty to support MDC directly,
     * we will use similar logic than Netty's LoggingHandler
     */
    private String format(ChannelHandlerContext ctx, String message) {
        InetSocketAddress local = (InetSocketAddress) ctx.channel().localAddress();
        InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress();

        String localhost;
        if(local != null) {
            localhost = local.getAddress().getHostAddress() + ":" + local.getPort();
        } else{
            localhost = "undefined";
        }

        String remotehost;
        if(remote != null) {
            remotehost = remote.getAddress().getHostAddress() + ":" + remote.getPort();
        } else{
            remotehost = "undefined";
        }

        return "[local: " + localhost + ", remote: " + remotehost + "] " + message;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy