Support Log4J2 MultiFormatStringBuilderFormattable structured messages

Update Log4J2 `ElasticCommonSchemaStructuredLogFormatter` and
`LogstashStructuredLogFormatter` to support Log4J2 JSON structured
messages (typically `MapMessage`)

Closes gh-42034
This commit is contained in:
Phillip Webb 2024-08-27 16:31:29 -07:00
parent 019dd678e6
commit ad730a6c84
5 changed files with 99 additions and 4 deletions

View File

@ -22,7 +22,6 @@ import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
@ -54,7 +53,7 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
members.add("process.thread.name", LogEvent::getThreadName);
ElasticCommonSchemaService.get(environment).jsonMembers(members);
members.add("log.logger", LogEvent::getLoggerName);
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));

View File

@ -27,7 +27,6 @@ import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
@ -51,7 +50,7 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<Lo
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
members.add("logger_name", LogEvent::getLoggerName);
members.add("thread_name", LogEvent::getThreadName);
members.add("level", LogEvent::getLevel).as(Level::name);

View File

@ -0,0 +1,66 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging.log4j2;
import java.io.IOException;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
import org.springframework.boot.json.JsonWriter.WritableJson;
/**
* Helper used to adapt {@link Message} for structured writing.
*
* @author Phillip Webb
*/
final class StructuredMessage {
private static final String JSON2 = "JSON";
private static final String[] JSON = { JSON2 };
private StructuredMessage() {
}
static Object get(Message message) {
if (message instanceof MultiFormatStringBuilderFormattable multiFormatMessage
&& hasJsonFormat(multiFormatMessage)) {
return WritableJson.of((out) -> formatTo(multiFormatMessage, out));
}
return message.getFormattedMessage();
}
private static boolean hasJsonFormat(MultiFormatStringBuilderFormattable message) {
for (String format : message.getFormats()) {
if (JSON2.equalsIgnoreCase(format)) {
return true;
}
}
return false;
}
private static void formatTo(MultiFormatStringBuilderFormattable message, Appendable out) throws IOException {
if (out instanceof StringBuilder stringBuilder) {
message.formatTo(JSON, stringBuilder);
}
else {
out.append(message.getFormattedMessage(JSON));
}
}
}

View File

@ -20,6 +20,7 @@ import java.util.Map;
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.message.MapMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -78,4 +79,18 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
}
@Test
void shouldFormatStructuredMessage() {
MutableLogEvent event = createEvent();
event.setMessage(new MapMessage<>().with("foo", true).with("bar", 1.0));
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
Map<String, Object> expectedMessage = Map.of("foo", true, "bar", 1.0);
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z",
"log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name",
"service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger",
"org.example.Test", "message", expectedMessage, "ecs.version", "8.11"));
}
}

View File

@ -25,6 +25,7 @@ import java.util.Map;
import org.apache.logging.log4j.MarkerManager.Log4jMarker;
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.message.MapMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -77,4 +78,19 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.LogstashStructuredLogFormatterTests.shouldFormatException""");
}
@Test
void shouldFormatStructuredMessage() {
MutableLogEvent event = createEvent();
event.setMessage(new MapMessage<>().with("foo", true).with("bar", 1.0));
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
Map<String, Object> expectedMessage = Map.of("foo", true, "bar", 1.0);
String timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME
.format(OffsetDateTime.ofInstant(EVENT_TIME, ZoneId.systemDefault()));
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(
map("@timestamp", timestamp, "@version", "1", "message", expectedMessage, "logger_name",
"org.example.Test", "thread_name", "main", "level", "INFO", "level_value", 400));
}
}