Skip to content

Commit ad730a6

Browse files
committed
Support Log4J2 MultiFormatStringBuilderFormattable structured messages
Update Log4J2 `ElasticCommonSchemaStructuredLogFormatter` and `LogstashStructuredLogFormatter` to support Log4J2 JSON structured messages (typically `MapMessage`) Closes gh-42034
1 parent 019dd67 commit ad730a6

File tree

5 files changed

+99
-4
lines changed

5 files changed

+99
-4
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.apache.logging.log4j.core.LogEvent;
2323
import org.apache.logging.log4j.core.impl.ThrowableProxy;
2424
import org.apache.logging.log4j.core.time.Instant;
25-
import org.apache.logging.log4j.message.Message;
2625
import org.apache.logging.log4j.util.ReadOnlyStringMap;
2726

2827
import org.springframework.boot.json.JsonWriter;
@@ -54,7 +53,7 @@ private static void jsonMembers(Environment environment, JsonWriter.Members<LogE
5453
members.add("process.thread.name", LogEvent::getThreadName);
5554
ElasticCommonSchemaService.get(environment).jsonMembers(members);
5655
members.add("log.logger", LogEvent::getLoggerName);
57-
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
56+
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
5857
members.from(LogEvent::getContextData)
5958
.whenNot(ReadOnlyStringMap::isEmpty)
6059
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.apache.logging.log4j.core.LogEvent;
2828
import org.apache.logging.log4j.core.impl.ThrowableProxy;
2929
import org.apache.logging.log4j.core.time.Instant;
30-
import org.apache.logging.log4j.message.Message;
3130
import org.apache.logging.log4j.util.ReadOnlyStringMap;
3231

3332
import org.springframework.boot.json.JsonWriter;
@@ -51,7 +50,7 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<Lo
5150
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
5251
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
5352
members.add("@version", "1");
54-
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
53+
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
5554
members.add("logger_name", LogEvent::getLoggerName);
5655
members.add("thread_name", LogEvent::getThreadName);
5756
members.add("level", LogEvent::getLevel).as(Level::name);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.logging.log4j2;
18+
19+
import java.io.IOException;
20+
21+
import org.apache.logging.log4j.message.Message;
22+
import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
23+
24+
import org.springframework.boot.json.JsonWriter.WritableJson;
25+
26+
/**
27+
* Helper used to adapt {@link Message} for structured writing.
28+
*
29+
* @author Phillip Webb
30+
*/
31+
final class StructuredMessage {
32+
33+
private static final String JSON2 = "JSON";
34+
35+
private static final String[] JSON = { JSON2 };
36+
37+
private StructuredMessage() {
38+
}
39+
40+
static Object get(Message message) {
41+
if (message instanceof MultiFormatStringBuilderFormattable multiFormatMessage
42+
&& hasJsonFormat(multiFormatMessage)) {
43+
return WritableJson.of((out) -> formatTo(multiFormatMessage, out));
44+
}
45+
return message.getFormattedMessage();
46+
}
47+
48+
private static boolean hasJsonFormat(MultiFormatStringBuilderFormattable message) {
49+
for (String format : message.getFormats()) {
50+
if (JSON2.equalsIgnoreCase(format)) {
51+
return true;
52+
}
53+
}
54+
return false;
55+
}
56+
57+
private static void formatTo(MultiFormatStringBuilderFormattable message, Appendable out) throws IOException {
58+
if (out instanceof StringBuilder stringBuilder) {
59+
message.formatTo(JSON, stringBuilder);
60+
}
61+
else {
62+
out.append(message.getFormattedMessage(JSON));
63+
}
64+
}
65+
66+
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
2222
import org.apache.logging.log4j.core.impl.MutableLogEvent;
23+
import org.apache.logging.log4j.message.MapMessage;
2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
2526

@@ -78,4 +79,18 @@ void shouldFormatException() {
7879
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
7980
}
8081

82+
@Test
83+
void shouldFormatStructuredMessage() {
84+
MutableLogEvent event = createEvent();
85+
event.setMessage(new MapMessage<>().with("foo", true).with("bar", 1.0));
86+
String json = this.formatter.format(event);
87+
assertThat(json).endsWith("\n");
88+
Map<String, Object> deserialized = deserialize(json);
89+
Map<String, Object> expectedMessage = Map.of("foo", true, "bar", 1.0);
90+
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z",
91+
"log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name",
92+
"service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger",
93+
"org.example.Test", "message", expectedMessage, "ecs.version", "8.11"));
94+
}
95+
8196
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatterTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.logging.log4j.MarkerManager.Log4jMarker;
2626
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
2727
import org.apache.logging.log4j.core.impl.MutableLogEvent;
28+
import org.apache.logging.log4j.message.MapMessage;
2829
import org.junit.jupiter.api.BeforeEach;
2930
import org.junit.jupiter.api.Test;
3031

@@ -77,4 +78,19 @@ void shouldFormatException() {
7778
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.LogstashStructuredLogFormatterTests.shouldFormatException""");
7879
}
7980

81+
@Test
82+
void shouldFormatStructuredMessage() {
83+
MutableLogEvent event = createEvent();
84+
event.setMessage(new MapMessage<>().with("foo", true).with("bar", 1.0));
85+
String json = this.formatter.format(event);
86+
assertThat(json).endsWith("\n");
87+
Map<String, Object> deserialized = deserialize(json);
88+
Map<String, Object> expectedMessage = Map.of("foo", true, "bar", 1.0);
89+
String timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME
90+
.format(OffsetDateTime.ofInstant(EVENT_TIME, ZoneId.systemDefault()));
91+
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(
92+
map("@timestamp", timestamp, "@version", "1", "message", expectedMessage, "logger_name",
93+
"org.example.Test", "thread_name", "main", "level", "INFO", "level_value", 400));
94+
}
95+
8096
}

0 commit comments

Comments
 (0)