/*
* Copyright 2014 TORCH GmbH
*
* 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 org.graylog2.gelfclient.encoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Random;
/**
* A Netty channel handler which splits large GELF messages into
* <a href="http://graylog2.org/gelf#specs">chunked GELF</a> messages.
*/
@ChannelHandler.Sharable
public class GelfMessageChunkEncoder extends MessageToMessageEncoder<ByteBuf> {
private static final Logger LOG = LoggerFactory.getLogger(GelfMessageChunkEncoder.class);
private static final int MAX_CHUNKS = 128;
private static final int MAX_CHUNK_SIZE = 1420;
private static final int MAX_MESSAGE_SIZE = (MAX_CHUNKS * MAX_CHUNK_SIZE);
private static final byte[] CHUNK_MAGIC_BYTES = new byte[]{0x1e, 0x0f};
private final byte[] machineIdentifier = new byte[4];
/**
* {@inheritDoc}
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
LOG.error("Chunking error", cause);
}
private class Chunker {
private final byte[] sequenceCount;
private int sequenceNumber = 0;
private final byte[] messageId = generateMessageId();
public Chunker(final int messageSize) {
int sequenceCount = (messageSize / MAX_CHUNK_SIZE);
// Check if we have to add another chunk due to integer division.
if ((messageSize % MAX_CHUNK_SIZE) != 0) {
sequenceCount++;
}
this.sequenceCount = new byte[]{(byte) sequenceCount};
}
public ByteBuf nextChunk(final ByteBuf chunk) {
final byte[] sequenceNumber = new byte[]{(byte) this.sequenceNumber++};
final byte[] data = new byte[chunk.readableBytes()];
chunk.readBytes(data);
LOG.debug("nextChunk bytes magicBytes={} messageId={} sequenceNumber={} sequenceCount={} data={}",
CHUNK_MAGIC_BYTES.length, messageId.length, sequenceNumber.length, sequenceCount.length, data.length
);
return Unpooled.copiedBuffer(CHUNK_MAGIC_BYTES, messageId, sequenceNumber, sequenceCount, data);
}
private byte[] generateMessageId() {
// GELF message ID, max 8 bytes
final ByteBuf messageId = Unpooled.buffer(8, 8);
// 4 bytes of current time.
messageId.writeInt((int) System.currentTimeMillis());
messageId.writeBytes(machineIdentifier, 0, 4);
return messageId.array();
}
}
/**
* Creates a new instance with a given machine identifier used in the generation of the message ID.
* <p>Usually the hostname of the client makes a good enough machine identifier.</p>
*
* @param machineIdentifier the machine identifier (only the first 4 bytes are being used)
*/
public GelfMessageChunkEncoder(final byte[] machineIdentifier) {
if (machineIdentifier.length < 4) {
throw new IllegalArgumentException("The machine identifier must at least be 4 bytes long.");
}
System.arraycopy(machineIdentifier, 0, this.machineIdentifier, 0, 4);
}
/**
* Creates a new instance with a random machine identifier used in the generation of the message ID.
*/
public GelfMessageChunkEncoder() {
this(randomIdentifier(4));
}
private static byte[] randomIdentifier(final int length) {
final byte[] randomIdentifier = new byte[length];
final Random random = new Random();
random.nextBytes(randomIdentifier);
return randomIdentifier;
}
/**
* {@inheritDoc}
*/
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception {
if (buf.readableBytes() > MAX_MESSAGE_SIZE) {
throw new EncoderException("Message too big. " + buf.readableBytes() + " bytes (max " + MAX_MESSAGE_SIZE + ")");
}
if (buf.readableBytes() <= MAX_CHUNK_SIZE) {
// Need to retain() the buffer here to avoid releasing the buffer too early.
out.add(buf.retain());
} else {
final Chunker chunker = new Chunker(buf.readableBytes());
try {
while (buf.readableBytes() > 0) {
if (buf.readableBytes() >= MAX_CHUNK_SIZE) {
out.add(chunker.nextChunk(buf.readSlice(MAX_CHUNK_SIZE)));
} else {
out.add(chunker.nextChunk(buf.readSlice(buf.readableBytes())));
}
}
} catch (Exception e) {
LOG.error("Chunk encoder error", e);
buf.release();
}
}
}
}