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:
parent
019dd678e6
commit
ad730a6c84
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue