diff --git a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java index 17ecd925..d0a073d4 100644 --- a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java +++ b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java @@ -53,6 +53,7 @@ public class EcsJsonSerializer { "event.dataset", "process.thread.name", "process.thread.id", + "event.sequence", "ecs.version")); public static CharSequence toNullSafeString(final CharSequence s) { @@ -144,6 +145,12 @@ public static void serializeEventDataset(StringBuilder builder, String eventData } } + public static void serializeEventSequence(StringBuilder builder, long sequenceNumber) { + builder.append("\"event.sequence\":"); + builder.append(sequenceNumber); + builder.append(","); + } + public static void serializeLogLevel(StringBuilder builder, String level) { builder.append("\"log.level\":"); // add padding so that all levels line up diff --git a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java index 59537070..5db84f70 100644 --- a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java +++ b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java @@ -226,6 +226,26 @@ void getMessageStringBuilderReuseNormallySizedBuffer() { assertThat(sb2.length()).isZero(); } + @Test + void serializeEventSequence() throws JsonProcessingException { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + EcsJsonSerializer.serializeEventSequence(jsonBuilder, 42); + EcsJsonSerializer.serializeObjectEnd(jsonBuilder); + JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString()); + assertThat(jsonNode.get("event.sequence").longValue()).isEqualTo(42); + } + + @Test + void serializeEventSequenceZero() throws JsonProcessingException { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + EcsJsonSerializer.serializeEventSequence(jsonBuilder, 0); + EcsJsonSerializer.serializeObjectEnd(jsonBuilder); + JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString()); + assertThat(jsonNode.get("event.sequence").longValue()).isEqualTo(0); + } + private void assertRemoveIfEndsWith(String builder, String ending, String expected) { StringBuilder sb = new StringBuilder(builder); EcsJsonSerializer.removeIfEndsWith(sb, ending); diff --git a/logback-ecs-encoder/src/main/java/co/elastic/logging/logback/EcsEncoder.java b/logback-ecs-encoder/src/main/java/co/elastic/logging/logback/EcsEncoder.java index 2a0e45c5..90bb0bb3 100644 --- a/logback-ecs-encoder/src/main/java/co/elastic/logging/logback/EcsEncoder.java +++ b/logback-ecs-encoder/src/main/java/co/elastic/logging/logback/EcsEncoder.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; @@ -45,6 +46,16 @@ public class EcsEncoder extends EncoderBase { private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final Method GET_SEQUENCE_NUMBER; + static { + Method m = null; + try { + m = ILoggingEvent.class.getMethod("getSequenceNumber"); + } catch (NoSuchMethodException ignored) { + } + GET_SEQUENCE_NUMBER = m; + } + private boolean emitEventSequence; private boolean stackTraceAsArray = false; private String serviceName; private String serviceVersion; @@ -72,6 +83,13 @@ public void start() { throwableConverter.start(); } eventDataset = EcsJsonSerializer.computeEventDataset(eventDataset, serviceName); + if (GET_SEQUENCE_NUMBER != null && getContext() != null) { + try { + Method getGenerator = getContext().getClass().getMethod("getSequenceNumberGenerator"); + emitEventSequence = getGenerator.invoke(getContext()) != null; + } catch (Exception ignored) { + } + } } /** @@ -115,6 +133,14 @@ public byte[] encode(ILoggingEvent event) { EcsJsonSerializer.serializeServiceEnvironment(builder, serviceEnvironment); EcsJsonSerializer.serializeServiceNodeName(builder, serviceNodeName); EcsJsonSerializer.serializeEventDataset(builder, eventDataset); + if (emitEventSequence) { + try { + EcsJsonSerializer.serializeEventSequence(builder, (Long) GET_SEQUENCE_NUMBER.invoke(event)); + } catch (Exception ignored) { + // Reflection call failed at runtime; disable for subsequent events + emitEventSequence = false; + } + } EcsJsonSerializer.serializeThreadName(builder, event.getThreadName()); EcsJsonSerializer.serializeLoggerName(builder, event.getLoggerName()); EcsJsonSerializer.serializeAdditionalFields(builder, additionalFields); diff --git a/logback-ecs-encoder/src/test/java/co/elastic/logging/logback/EcsEncoderTest.java b/logback-ecs-encoder/src/test/java/co/elastic/logging/logback/EcsEncoderTest.java index 13f10e51..7a4acfee 100644 --- a/logback-ecs-encoder/src/test/java/co/elastic/logging/logback/EcsEncoderTest.java +++ b/logback-ecs-encoder/src/test/java/co/elastic/logging/logback/EcsEncoderTest.java @@ -31,6 +31,9 @@ import java.io.IOException; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + public class EcsEncoderTest extends AbstractEcsEncoderTest { private OutputStreamAppender appender; @@ -61,4 +64,13 @@ void setUp() { public JsonNode getLastLogLine() throws IOException { return objectMapper.readTree(appender.getBytes()); } + + @Test + void testEventSequenceAbsentWithoutGenerator() throws Exception { + logger.debug("test"); + JsonNode logLine = getLastLogLine(); + // No SequenceNumberGenerator on the context (and logback 1.2.x lacks + // the API entirely), so event.sequence must not appear. + assertThat(logLine.has("event.sequence")).isFalse(); + } }