diff --git a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/LogBufferSerializer.java b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/LogBufferSerializer.java index dfb2cd9..1362858 100644 --- a/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/LogBufferSerializer.java +++ b/tjahzi-core/src/main/java/pl/tkowalcz/tjahzi/LogBufferSerializer.java @@ -13,6 +13,19 @@ public class LogBufferSerializer { this.buffer = buffer; } + public int calculateRequiredSizeAscii( + Map labels, + String logLevelLabel, + String logLevel, + ByteBuffer line + ) { + int logLineSize = Integer.BYTES + line.remaining(); + int logLevelSize = calculateOptionalLabelSizeAscii(logLevelLabel, logLevel); + int headerAndLabelsSize = calculateRequiredSizeAscii(labels); + + return headerAndLabelsSize + logLineSize + logLevelSize; + } + public void writeTo( int cursor, long timestamp, @@ -21,16 +34,54 @@ public class LogBufferSerializer { String logLevel, ByteBuffer line ) { - cursor = writeHeader(cursor, timestamp, labels); + cursor = writeHeader( + cursor, + timestamp, + labels, + logLevelLabel != null + ); + cursor = writeLabels(cursor, labels); - cursor = writeLabel( + cursor = writeOptionalLabel( cursor, logLevelLabel, logLevel ); + writeLogLine(cursor, line); } + private int writeHeader( + int cursor, + long timestamp, + Map labels, + boolean hasLogLevelLabel + ) { + buffer.putLong(cursor, timestamp); + cursor += Long.BYTES; + + int labelsCount = labels.size(); + if (hasLogLevelLabel) { + labelsCount++; + } + + buffer.putInt(cursor, labelsCount); + cursor += Integer.BYTES; + return cursor; + } + + private int writeLabels(int cursor, Map labels) { + for (Map.Entry entry : labels.entrySet()) { + cursor = writeLabel( + cursor, + entry.getKey(), + entry.getValue() + ); + } + + return cursor; + } + private void writeLogLine(int cursor, ByteBuffer line) { buffer.putInt(cursor, line.remaining()); cursor += Integer.BYTES; @@ -42,27 +93,7 @@ public class LogBufferSerializer { ); } - private int writeLabels(int cursor, Map labels) { - for (Map.Entry entry : labels.entrySet()) { - cursor = writeLabel( - cursor, - entry.getKey(), - entry.getValue() - ); - } - return cursor; - } - - private int writeHeader(int cursor, long timestamp, Map labels) { - buffer.putLong(cursor, timestamp); - cursor += Long.BYTES; - - buffer.putInt(cursor, labels.size() + 1); - cursor += Integer.BYTES; - return cursor; - } - - public int writeLabel( + private int writeLabel( int cursor, String key, String value @@ -73,32 +104,42 @@ public class LogBufferSerializer { return cursor; } + private int writeOptionalLabel( + int cursor, + String key, + String value + ) { + if (key != null) { + cursor += buffer.putStringAscii(cursor, key); + cursor += buffer.putStringAscii(cursor, value); + } + + return cursor; + } + private int calculateRequiredSizeAscii(Map labels) { int timestampSize = Long.BYTES; int labelsCountSize = Integer.BYTES; int sum = timestampSize + labelsCountSize; for (Map.Entry entry : labels.entrySet()) { - sum += calculateLabeSizeAscii(entry.getKey(), entry.getValue()); + sum += calculateLabelSizeAscii(entry.getKey(), entry.getValue()); } return sum; } - private int calculateLabeSizeAscii(String key, String value) { + private int calculateOptionalLabelSizeAscii(String key, String value) { + if (key == null) { + return 0; + } + return Integer.BYTES + key.length() + Integer.BYTES + value.length(); } - public int calculateRequiredSizeAscii( - Map labels, - String logLevelLabel, - String logLevel, - ByteBuffer line - ) { - int lineSize = Integer.BYTES + line.remaining(); - int logLevelSize = calculateLabeSizeAscii(logLevelLabel, logLevel); - - return calculateRequiredSizeAscii(labels) + lineSize + logLevelSize; + private int calculateLabelSizeAscii(String key, String value) { + return Integer.BYTES + key.length() + + Integer.BYTES + value.length(); } } diff --git a/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/LogBufferSerializerTest.java b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/LogBufferSerializerTest.java new file mode 100644 index 0000000..d5c4759 --- /dev/null +++ b/tjahzi-core/src/test/java/pl/tkowalcz/tjahzi/LogBufferSerializerTest.java @@ -0,0 +1,115 @@ +package pl.tkowalcz.tjahzi; + +import org.agrona.concurrent.UnsafeBuffer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class LogBufferSerializerTest { + + private static Stream variousMessageConfigurations() { + return Stream.of( + Arguments.of( + "Message with all fields filled", + Map.of("ip", "10.0.0.42", + "hostname", "razor-crest", + "region", "outer-rim" + ), + "log_level", + "WARN", + ByteBuffer.wrap("[Mando] You have something I want.".getBytes()), + 38 // log line + + 21 // log level + + 45 // labels string sizes + + 6 * 4 // labels string length indicators + + 8 // timestamp + + 4 // labels count + ), + Arguments.of( + "Message with empty labels", + Map.of(), + "log_level", + "WARN", + ByteBuffer.wrap("[Mando] You have something I want.".getBytes()), + 38 // log line + + 21 // log level + + 8 // timestamp + + 4 // labels count + ), + Arguments.of( + "Message with null log level", + Map.of("ip", "10.0.0.42", + "hostname", "razor-crest", + "region", "outer-rim" + ), + null, + null, + ByteBuffer.wrap("[Mando] You have something I want.".getBytes()), + 38 // log line + + 45 // labels string sizes + + 6 * 4 // labels string length indicators + + 8 // timestamp + + 4 // labels count + ) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("variousMessageConfigurations") + void shouldCalculateSizeOfMessage( + String testName, + Map labels, + String logLevelLabel, + String logLevel, + ByteBuffer logLine, + int expectedSize) { + // Given + UnsafeBuffer buffer = new UnsafeBuffer(new byte[expectedSize]); + LogBufferSerializer serializer = new LogBufferSerializer(buffer); + + // When + int actual = serializer.calculateRequiredSizeAscii( + labels, + logLevelLabel, + logLevel, + logLine + ); + + // Then + assertThat(actual).isEqualTo(expectedSize); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("variousMessageConfigurations") + void shouldSerializeMessage( + String testName, + Map labels, + String logLevelLabel, + String logLevel, + ByteBuffer logLine, + int expectedSize) { + // Given + UnsafeBuffer buffer = new UnsafeBuffer(new byte[expectedSize]); + LogBufferSerializer serializer = new LogBufferSerializer(buffer); + + // When + serializer.writeTo( + 0, + 42L, + labels, + logLevelLabel, + logLevel, + logLine + ); + + // Then + // There was no buffer overflow so seems legit. + // TODO: verify contents after extracting deserialization code to separate class. + } +}