From b12bed2b363d583a008178c328231e707526fbc3 Mon Sep 17 00:00:00 2001 From: Adnan Al Zahabi <99538471+zadnan2002@users.noreply.github.com> Date: Sat, 2 May 2026 11:07:10 +0300 Subject: [PATCH 1/2] [logback-ecs-encoder] Emit event.sequence when SequenceNumberGenerator is configured Add support for the ECS event.sequence field in the logback encoder. When running on logback 1.3+ with a SequenceNumberGenerator configured, the encoder now emits "event.sequence": for each log event with a non-zero sequence number. Uses reflection to probe for ILoggingEvent.getSequenceNumber() at class load time, maintaining full backward compatibility with logback 1.2.x (zero overhead when the method is absent). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../co/elastic/logging/EcsJsonSerializer.java | 7 +++++++ .../logging/EcsJsonSerializerTest.java | 20 +++++++++++++++++++ .../elastic/logging/logback/EcsEncoder.java | 19 ++++++++++++++++++ .../logging/logback/EcsEncoderTest.java | 12 +++++++++++ 4 files changed, 58 insertions(+) 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..13fe3fa2 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,15 @@ 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 stackTraceAsArray = false; private String serviceName; private String serviceVersion; @@ -115,6 +125,15 @@ public byte[] encode(ILoggingEvent event) { EcsJsonSerializer.serializeServiceEnvironment(builder, serviceEnvironment); EcsJsonSerializer.serializeServiceNodeName(builder, serviceNodeName); EcsJsonSerializer.serializeEventDataset(builder, eventDataset); + if (GET_SEQUENCE_NUMBER != null) { + try { + long seq = (Long) GET_SEQUENCE_NUMBER.invoke(event); + if (seq != 0) { + EcsJsonSerializer.serializeEventSequence(builder, seq); + } + } catch (Exception ignored) { + } + } 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..28720d8c 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(); + // On logback 1.2.x, ILoggingEvent has no getSequenceNumber() method, + // so event.sequence must not appear in the output. + assertThat(logLine.has("event.sequence")).isFalse(); + } } From d8958e04cf2e529c61dc55755ffb8603f757d338 Mon Sep 17 00:00:00 2001 From: Adnan Al Zahabi <99538471+zadnan2002@users.noreply.github.com> Date: Sun, 3 May 2026 11:48:00 +0300 Subject: [PATCH 2/2] fix: detect SequenceNumberGenerator on context instead of using seq==0 sentinel The previous approach skipped event.sequence when the value was 0, but 0 is a valid sequence number (first event). Now checks at start() whether the LoggerContext has a SequenceNumberGenerator registered (logback 1.3+ API), and disables on first reflection failure rather than silently dropping fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../co/elastic/logging/logback/EcsEncoder.java | 17 ++++++++++++----- .../elastic/logging/logback/EcsEncoderTest.java | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) 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 13fe3fa2..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 @@ -55,6 +55,7 @@ public class EcsEncoder extends EncoderBase { } GET_SEQUENCE_NUMBER = m; } + private boolean emitEventSequence; private boolean stackTraceAsArray = false; private String serviceName; private String serviceVersion; @@ -82,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) { + } + } } /** @@ -125,13 +133,12 @@ public byte[] encode(ILoggingEvent event) { EcsJsonSerializer.serializeServiceEnvironment(builder, serviceEnvironment); EcsJsonSerializer.serializeServiceNodeName(builder, serviceNodeName); EcsJsonSerializer.serializeEventDataset(builder, eventDataset); - if (GET_SEQUENCE_NUMBER != null) { + if (emitEventSequence) { try { - long seq = (Long) GET_SEQUENCE_NUMBER.invoke(event); - if (seq != 0) { - EcsJsonSerializer.serializeEventSequence(builder, seq); - } + 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()); 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 28720d8c..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 @@ -69,8 +69,8 @@ public JsonNode getLastLogLine() throws IOException { void testEventSequenceAbsentWithoutGenerator() throws Exception { logger.debug("test"); JsonNode logLine = getLastLogLine(); - // On logback 1.2.x, ILoggingEvent has no getSequenceNumber() method, - // so event.sequence must not appear in the output. + // 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(); } }