Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class EcsJsonSerializer {
private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<StringBuilder>();
private static final String NEW_LINE = System.getProperty("line.separator");
private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\\r\\n|\\n|\\r");
private static final int INITIAL_BUFFER_CAPACITY = 1024;
static final int MAX_BUFFER_CAPACITY = 8192;

public static CharSequence toNullSafeString(final CharSequence s) {
return s == null ? "" : s;
Expand Down Expand Up @@ -335,13 +337,23 @@ public static boolean endsWith(StringBuilder sb, String ending) {
return true;
}

/**
* Returns a thread-local {@link StringBuilder} for temporary message formatting.
* <p>
* If the buffer has grown beyond {@link #MAX_BUFFER_CAPACITY} (e.g. due to a large stack trace),
* it is discarded and replaced with a fresh instance to prevent unbounded memory retention in
* long-lived thread-pool threads.
*
* @see <a href="https://github.com/elastic/ecs-logging-java/issues/381">#381</a>
*/
public static StringBuilder getMessageStringBuilder() {
StringBuilder result = messageStringBuilder.get();
if (result == null) {
result = new StringBuilder(1024);
if (result == null || result.capacity() > MAX_BUFFER_CAPACITY) {
result = new StringBuilder(INITIAL_BUFFER_CAPACITY);
messageStringBuilder.set(result);
} else {
result.setLength(0);
}
result.setLength(0);
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,38 @@ void serializeExceptionWithNullMessage() throws JsonProcessingException {
assertThat(jsonNode.get(ERROR_MESSAGE)).isNull();
}

@Test
void getMessageStringBuilderDiscardsOversizedBuffer() {
// First call: initializes the thread-local StringBuilder
StringBuilder sb1 = EcsJsonSerializer.getMessageStringBuilder();
assertThat(sb1.capacity()).isLessThanOrEqualTo(EcsJsonSerializer.MAX_BUFFER_CAPACITY);

// Simulate a large log message / stack trace that bloats the buffer
sb1.append("x".repeat(EcsJsonSerializer.MAX_BUFFER_CAPACITY + 1));
assertThat(sb1.capacity()).isGreaterThan(EcsJsonSerializer.MAX_BUFFER_CAPACITY);

// Next call should detect the oversized buffer and replace it
StringBuilder sb2 = EcsJsonSerializer.getMessageStringBuilder();
assertThat(sb2).isNotSameAs(sb1);
assertThat(sb2.capacity()).isLessThanOrEqualTo(EcsJsonSerializer.MAX_BUFFER_CAPACITY);
assertThat(sb2.length()).isZero();
}

@Test
void getMessageStringBuilderReuseNormallySizedBuffer() {
// First call: initializes the thread-local StringBuilder
StringBuilder sb1 = EcsJsonSerializer.getMessageStringBuilder();

// Append something that stays within the threshold
sb1.append("small message");
assertThat(sb1.capacity()).isLessThanOrEqualTo(EcsJsonSerializer.MAX_BUFFER_CAPACITY);

// Next call should reuse the same instance (just cleared)
StringBuilder sb2 = EcsJsonSerializer.getMessageStringBuilder();
assertThat(sb2).isSameAs(sb1);
assertThat(sb2.length()).isZero();
}

private void assertRemoveIfEndsWith(String builder, String ending, String expected) {
StringBuilder sb = new StringBuilder(builder);
EcsJsonSerializer.removeIfEndsWith(sb, ending);
Expand Down
Loading