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

me.moocar.logbackgelf.GelfChunkingOutputStream Maven / Gradle / Ivy

package me.moocar.logbackgelf;

import java.io.IOException;
import java.io.OutputStream;
import java.net.*;

/**
 * An OutputStream that sends bytes as UDP packets to a [GELF](https://www.graylog.org/resources/gelf-2/) compatible
 * remote server. Use flush() to signify the end of a message. If the total number of bytes in the message is less than
 * maxPacketSize, they will be sent in one datagram. If more, the bytes will be broken up into GELF chunks and sent all
 * at once when flush() is called.
 *
 * Note that this class is NOT thread safe. A sequential process should call flush() before another starts writing.
 */
public class GelfChunkingOutputStream extends OutputStream {

    // GELF specifies a maximum number of chunks. Chunks will be dropped on the server once more than MAX_CHUNKS are
    // sent to the server for a particular message
    private final int MAX_CHUNKS = 128;
    private final byte[] CHUNKED_GELF_ID_BYTES = new byte[]{0x1e, 0x0f};
    private final int MESSAGE_ID_LENGTH = 8;
    private final int SEQ_COUNT_POSITION = CHUNKED_GELF_ID_BYTES.length + MESSAGE_ID_LENGTH + 1;

    // Bytes will be be added to this byte array until maxPacketSize is reached, at which point chunk mode will be
    // turned on, and the bytes will be replayed
    private final byte[] packetBytes;
    // Once chunk mode has been turned on (once maxPacketSize bytes have been written to this output stream), bytes will
    // be added to this 2D byte array. The first dimension is the current chunk sequence we're up to. The second
    // dimension is the bytes in that particular chunk
    private final byte[][] chunks;

    private final int maxPacketSize;
    private final MessageIdProvider messageIdProvider;

    private final InetAddress address;
    private final int port;
    private DatagramSocket socket;

    // When in chunking mode, this is the index of the chunk that we are currently writing bytes to
    private int chunkIndex = 0;
    // The position in the chunk or packetBytes that the next byte will be put into
    private int position = 0;
    // After a flush, chunking is turned off. It is only turned on once more than maxPacketSize bytes are written to
    // this output stream
    private boolean chunked = false;
    // True when more so many bytes have been written that we've exceeded MAX_CHUNKS
    private boolean maxChunksReached = false;
    // Once chunking is turned on, this will have the messageID that should be used for all of this message's chunks
    private byte[] messageID;

    /**
     * Create a new GelfChunkingOutputStream
     *
     * @param address The address of the remove server
     * @param port The port of the remote server
     * @param maxPacketSize The maximum number of bytes allowed before chunking begins
     * @param messageIdProvider A object that generates totally unique (for this machine) 8-byte message IDs.
     */
    public GelfChunkingOutputStream(InetAddress address, int port, int maxPacketSize, MessageIdProvider messageIdProvider) {
        this.address = address;
        this.port = port;
        this.maxPacketSize = maxPacketSize;
        this.messageIdProvider = messageIdProvider;
        this.chunks = new byte[MAX_CHUNKS][maxPacketSize];
        this.packetBytes = new byte[maxPacketSize];
    }


    public void start() throws SocketException, UnknownHostException {
        this.socket = new DatagramSocket();
        this.socket.connect(address, port);
    }

    @Override
    public void write(int b) throws IOException {
        if (maxChunksReached) {
            return;
        }
        if (!chunked) {
            writeUnchunked((byte) b);

        } else {
            writeChunked((byte) b);
        }
    }

    private void writeUnchunked(byte b) throws IOException {
        if (position < maxPacketSize) {
            packetBytes[position++] = b;
        } else {
            startChunking();
            write(b);
        }
    }

    private void startChunking() throws IOException {
        chunked = true;
        messageID = messageIdProvider.get();
        chunkIndex = -1;
        // pour the bytes back through in chunking mode. packetBytes is now ignored until flush finishes.
        for (byte packetByte : packetBytes) {
            write(packetByte);
        }

    }

    private void writeChunked(byte b) throws IOException {
        if (position == maxPacketSize) {
            if (chunkIndex == MAX_CHUNKS - 1) {
                maxChunksReached = true;
            } else {
                chunkIndex++;
                position = 0;
                writeHeader();
                write(b);
            }
        } else {
            chunks[chunkIndex][position++] = b;
        }
    }

    private void writeHeader() {
        chunks[chunkIndex][position++] = CHUNKED_GELF_ID_BYTES[0];
        chunks[chunkIndex][position++] = CHUNKED_GELF_ID_BYTES[1];
        for (byte messageIDByte : messageID) {
            chunks[chunkIndex][position++] = messageIDByte;
        }
        chunks[chunkIndex][position++] = (byte) chunkIndex;
        position++; // for the sequence count which will be added in flush
    }

    @Override
    public void flush() throws IOException {
        try {
            // Calling flush but write has not been called. do nothing
            if (chunkIndex == 0 && position == 0) {
                return;
            }

            // Too many bytes written. Drop everything
            if (maxChunksReached) {
                return;
            }

            if (chunked) {
                flushChunked();

            } else {
                sendBytes(packetBytes, position);
            }

        } finally {
            reset();
        }

    }

    private void flushChunked() throws IOException {
        fillInSequenceCounts();
        for (int i = 0; i < chunkIndex; i++) {
            sendBytes(chunks[i], maxPacketSize);
        }
        sendBytes(chunks[chunkIndex], position);

    }

    private void fillInSequenceCounts() {
        for (int i = 0; i <= chunkIndex; i++) {
            chunks[i][SEQ_COUNT_POSITION] = (byte) (chunkIndex + 1);
        }
    }

    private void sendBytes(byte[] bytes, int length) throws IOException {
        DatagramPacket packet = new DatagramPacket(bytes, 0, length);
        socket.send(packet);
    }

    private void reset() {
        position = 0;
        chunkIndex = 0;
        chunked = false;
        maxChunksReached = false;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy