package org.graylog2; import org.json.simple.JSONValue; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.zip.GZIPOutputStream; public class GelfMessage { private static final String ID_NAME = "id"; private static final String GELF_VERSION = "1.1"; private static final byte[] GELF_CHUNKED_ID = new byte[]{0x1e, 0x0f}; private static final int MAXIMUM_CHUNK_SIZE = 1420; private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000); private String version = GELF_VERSION; private String host; private byte[] hostBytes = lastFourAsciiBytes("none"); private String shortMessage; private String fullMessage; private long javaTimestamp; private String level; private String facility = "gelf-java"; private String line; private String file; private Map<String, Object> additonalFields = new HashMap<String, Object>(); public GelfMessage() { } public GelfMessage(String shortMessage, String fullMessage, long timestamp, String level) { this(shortMessage, fullMessage, timestamp, level, null, null); } public GelfMessage(String shortMessage, String fullMessage, long timestamp, String level, String line, String file) { this.shortMessage = shortMessage != null ? shortMessage : "null"; this.fullMessage = fullMessage; this.javaTimestamp = timestamp; this.level = level; this.line = line; this.file = file; } public String toJson() { Map<String, Object> map = new HashMap<String, Object>(); map.put("version", getVersion()); map.put("host", getHost()); map.put("short_message", getShortMessage()); map.put("full_message", getFullMessage()); map.put("timestamp", getTimestamp()); map.put("facility", getFacility()); try { map.put("level", Long.parseLong(getLevel())); } catch (NumberFormatException e) { map.put("level", 6L); // fallback to info } if (null != getFile()) { map.put("file", getFile()); } if (null != getLine()) { try { map.put("line", Long.parseLong(getLine())); } catch (NumberFormatException e) { map.put("line", -1L); } } for (Map.Entry<String, Object> additionalField : additonalFields.entrySet()) { if (!ID_NAME.equals(additionalField.getKey())) { map.put("_" + additionalField.getKey(), additionalField.getValue()); } } return JSONValue.toJSONString(map); } public ByteBuffer[] toUDPBuffers() { byte[] messageBytes = gzipMessage(toJson()); // calculate the length of the datagrams array int diagrams_length = messageBytes.length / MAXIMUM_CHUNK_SIZE; // In case of a remainder, due to the integer division, add a extra datagram if (messageBytes.length % MAXIMUM_CHUNK_SIZE != 0) { diagrams_length++; } ByteBuffer[] datagrams = new ByteBuffer[diagrams_length]; if (messageBytes.length > MAXIMUM_CHUNK_SIZE) { sliceDatagrams(messageBytes, datagrams); } else { datagrams[0] = ByteBuffer.allocate(messageBytes.length); datagrams[0].put(messageBytes); datagrams[0].flip(); } return datagrams; } public ByteBuffer toTCPBuffer() { byte[] messageBytes; try { // Do not use GZIP, as the headers will contain \0 bytes // graylog2-server uses \0 as a delimiter for TCP frames // see: https://github.com/Graylog2/graylog2-server/issues/127 String json = toJson() ; json += '\0'; messageBytes = json.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("No UTF-8 support available.", e); } ByteBuffer buffer = ByteBuffer.allocate(messageBytes.length); buffer.put(messageBytes); buffer.flip(); return buffer; } public ByteBuffer toAMQPBuffer() { byte[] messageBytes = gzipMessage(toJson()); ByteBuffer buffer = ByteBuffer.allocate(messageBytes.length); buffer.put(messageBytes); buffer.flip(); return buffer; } private void sliceDatagrams(byte[] messageBytes, ByteBuffer[] datagrams) { int messageLength = messageBytes.length; byte[] messageId = new byte[8]; new Random().nextBytes(messageId); // Reuse length of datagrams array since this is supposed to be the correct number of datagrams int num = datagrams.length; for (int idx = 0; idx < num; idx++) { byte[] header = concatByteArray(GELF_CHUNKED_ID, concatByteArray(messageId, new byte[]{(byte) idx, (byte) num})); int from = idx * MAXIMUM_CHUNK_SIZE; int to = from + MAXIMUM_CHUNK_SIZE; if (to >= messageLength) { to = messageLength; } byte[] range = new byte[to - from]; System.arraycopy(messageBytes, from, range, 0, range.length); byte[] datagram = concatByteArray(header, range); datagrams[idx] = ByteBuffer.allocate(datagram.length); datagrams[idx].put(datagram); datagrams[idx].flip(); } } public int getCurrentMillis() { return (int) System.currentTimeMillis(); } private byte[] gzipMessage(String message) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { GZIPOutputStream stream = new GZIPOutputStream(bos); byte[] bytes; try { bytes = message.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("No UTF-8 support available.", e); } stream.write(bytes); stream.finish(); stream.close(); byte[] zipped = bos.toByteArray(); bos.close(); return zipped; } catch (IOException e) { return null; } } private byte[] lastFourAsciiBytes(String host) { final String shortHost = host.length() >= 4 ? host.substring(host.length() - 4) : host; try { return shortHost.getBytes("ASCII"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("JVM without ascii support?", e); } } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getHost() { return host; } public void setHost(String host) { this.host = host; this.hostBytes = lastFourAsciiBytes(host); } public String getShortMessage() { return shortMessage; } public void setShortMessage(String shortMessage) { this.shortMessage = shortMessage; } public String getFullMessage() { return fullMessage; } public void setFullMessage(String fullMessage) { this.fullMessage = fullMessage; } public String getTimestamp() { return new BigDecimal(javaTimestamp).divide(TIME_DIVISOR).toPlainString(); } public Long getJavaTimestamp() { return javaTimestamp; } public void setJavaTimestamp(long javaTimestamp) { this.javaTimestamp = javaTimestamp; } public String getLevel() { return level; } public void setLevel(String level) { this.level = level; } public String getFacility() { return facility; } public void setFacility(String facility) { this.facility = facility; } public String getLine() { return line; } public void setLine(String line) { this.line = line; } public String getFile() { return file; } public void setFile(String file) { this.file = file; } public GelfMessage addField(String key, String value) { getAdditonalFields().put(key, value); return this; } public GelfMessage addField(String key, Object value) { getAdditonalFields().put(key, value); return this; } public Map<String, Object> getAdditonalFields() { return additonalFields; } public void setAdditonalFields(Map<String, Object> additonalFields) { this.additonalFields = new HashMap<String, Object>(additonalFields); } public boolean isValid() { return isShortOrFullMessagesExists() && !isEmpty(version) && !isEmpty(host) && !isEmpty(facility); } private boolean isShortOrFullMessagesExists() { return shortMessage != null || fullMessage != null; } public boolean isEmpty(String str) { return str == null || "".equals(str.trim()); } byte[] concatByteArray(byte[] first, byte[] second) { byte[] result = new byte[first.length + second.length]; System.arraycopy(first, 0, result, 0, first.length); System.arraycopy(second, 0, result, first.length, second.length); return result; } }