Used nested format for ECS structure logging
Update `ElasticCommonSchemaStructuredLogFormatter` implementations so that nested JSON is used for entries that previous has a '.' in the name. This format follows the ECS specification and should be compatible with more backends. Fixes gh-45063
This commit is contained in:
parent
baa8d1a333
commit
5b165b35e3
|
@ -504,7 +504,7 @@ A log line looks like this:
|
|||
|
||||
[source,json]
|
||||
----
|
||||
{"@timestamp":"2024-01-01T10:15:00.067462556Z","log.level":"INFO","process.pid":39599,"process.thread.name":"main","service.name":"simple","log.logger":"org.example.Application","message":"No active profile set, falling back to 1 default profile: \"default\"","ecs.version":"8.11"}
|
||||
{"@timestamp":"2024-01-01T10:15:00.067462556Z","log":{"level":"INFO","logger":"org.example.Application"},"process":{"pid":39599,"thread":{"name":"main"}},"service":{"name":"simple"},"message":"No active profile set, falling back to 1 default profile: \"default\"","ecs":{"version":"8.11"}}
|
||||
----
|
||||
|
||||
This format also adds every key value pair contained in the MDC to the JSON object.
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.logging.log4j2;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
@ -28,9 +29,9 @@ import org.apache.logging.log4j.core.time.Instant;
|
|||
import org.apache.logging.log4j.util.ReadOnlyStringMap;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.Members;
|
||||
import org.springframework.boot.logging.StackTracePrinter;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaPairs;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
|
@ -56,36 +57,38 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
|
|||
JsonWriter.Members<LogEvent> members) {
|
||||
Extractor extractor = new Extractor(stackTracePrinter);
|
||||
members.add("@timestamp", LogEvent::getInstant).as(ElasticCommonSchemaStructuredLogFormatter::asTimestamp);
|
||||
members.add("log.level", LogEvent::getLevel).as(Level::name);
|
||||
members.add("process.pid", environment.getProperty("spring.application.pid", Long.class))
|
||||
.when(Objects::nonNull);
|
||||
members.add("process.thread.name", LogEvent::getThreadName);
|
||||
members.add("log").usingMembers((log) -> {
|
||||
log.add("level", LogEvent::getLevel).as(Level::name);
|
||||
log.add("logger", LogEvent::getLoggerName);
|
||||
});
|
||||
members.add("process").usingMembers((process) -> {
|
||||
process.add("pid", environment.getProperty("spring.application.pid", Long.class)).when(Objects::nonNull);
|
||||
process.add("thread").usingMembers((thread) -> thread.add("name", LogEvent::getThreadName));
|
||||
});
|
||||
ElasticCommonSchemaProperties.get(environment).jsonMembers(members);
|
||||
members.add("log.logger", LogEvent::getLoggerName);
|
||||
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
|
||||
members.from(LogEvent::getContextData)
|
||||
.whenNot(ReadOnlyStringMap::isEmpty)
|
||||
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
|
||||
members.from(LogEvent::getThrownProxy)
|
||||
.whenNotNull()
|
||||
.usingMembers((thrownProxyMembers) -> throwableMembers(thrownProxyMembers, extractor));
|
||||
.as((contextData) -> ElasticCommonSchemaPairs.nested((nested) -> contextData.forEach(nested::accept)))
|
||||
.usingPairs(Map::forEach);
|
||||
members.from(LogEvent::getThrownProxy).whenNotNull().usingMembers((thrownProxyMembers) -> {
|
||||
thrownProxyMembers.add("error").usingMembers((error) -> {
|
||||
error.add("type", ThrowableProxy::getThrowable).whenNotNull().as(ObjectUtils::nullSafeClassName);
|
||||
error.add("message", ThrowableProxy::getMessage);
|
||||
error.add("stack_trace", extractor::stackTrace);
|
||||
});
|
||||
});
|
||||
members.add("tags", LogEvent::getMarker)
|
||||
.whenNotNull()
|
||||
.as(ElasticCommonSchemaStructuredLogFormatter::getMarkers)
|
||||
.whenNotEmpty();
|
||||
members.add("ecs.version", "8.11");
|
||||
members.add("ecs").usingMembers((ecs) -> ecs.add("version", "8.11"));
|
||||
}
|
||||
|
||||
private static java.time.Instant asTimestamp(Instant instant) {
|
||||
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
|
||||
}
|
||||
|
||||
private static void throwableMembers(Members<ThrowableProxy> members, Extractor extractor) {
|
||||
members.add("error.type", ThrowableProxy::getThrowable).whenNotNull().as(ObjectUtils::nullSafeClassName);
|
||||
members.add("error.message", ThrowableProxy::getMessage);
|
||||
members.add("error.stack_trace", extractor::stackTrace);
|
||||
}
|
||||
|
||||
private static Set<String> getMarkers(Marker marker) {
|
||||
Set<String> result = new TreeSet<>();
|
||||
addMarkers(result, marker);
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.logging.logback;
|
|||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
@ -29,9 +30,9 @@ import org.slf4j.Marker;
|
|||
import org.slf4j.event.KeyValuePair;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.PairExtractor;
|
||||
import org.springframework.boot.logging.StackTracePrinter;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaPairs;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
|
@ -47,9 +48,6 @@ import org.springframework.core.env.Environment;
|
|||
*/
|
||||
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
|
||||
|
||||
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
|
||||
(pair) -> pair.value);
|
||||
|
||||
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
|
||||
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(environment, stackTracePrinter, throwableProxyConverter, members), customizer);
|
||||
|
@ -59,27 +57,41 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
|
|||
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
|
||||
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
|
||||
members.add("@timestamp", ILoggingEvent::getInstant);
|
||||
members.add("log.level", ILoggingEvent::getLevel);
|
||||
members.add("process.pid", environment.getProperty("spring.application.pid", Long.class))
|
||||
.when(Objects::nonNull);
|
||||
members.add("process.thread.name", ILoggingEvent::getThreadName);
|
||||
members.add("log").usingMembers((log) -> {
|
||||
log.add("level", ILoggingEvent::getLevel);
|
||||
log.add("logger", ILoggingEvent::getLoggerName);
|
||||
});
|
||||
members.add("process").usingMembers((process) -> {
|
||||
process.add("pid", environment.getProperty("spring.application.pid", Long.class)).when(Objects::nonNull);
|
||||
process.add("thread").usingMembers((thread) -> thread.add("name", ILoggingEvent::getThreadName));
|
||||
});
|
||||
ElasticCommonSchemaProperties.get(environment).jsonMembers(members);
|
||||
members.add("log.logger", ILoggingEvent::getLoggerName);
|
||||
members.add("message", ILoggingEvent::getFormattedMessage);
|
||||
members.addMapEntries(ILoggingEvent::getMDCPropertyMap);
|
||||
members.from(ILoggingEvent::getMDCPropertyMap)
|
||||
.whenNotEmpty()
|
||||
.as(ElasticCommonSchemaPairs::nested)
|
||||
.usingPairs(Map::forEach);
|
||||
members.from(ILoggingEvent::getKeyValuePairs)
|
||||
.whenNotEmpty()
|
||||
.usingExtractedPairs(Iterable::forEach, keyValuePairExtractor);
|
||||
.as(ElasticCommonSchemaStructuredLogFormatter::nested)
|
||||
.usingPairs(Map::forEach);
|
||||
members.add().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> {
|
||||
throwableMembers.add("error.type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
|
||||
throwableMembers.add("error.message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
|
||||
throwableMembers.add("error.stack_trace", extractor::stackTrace);
|
||||
throwableMembers.add("error").usingMembers((error) -> {
|
||||
error.add("type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
|
||||
error.add("message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
|
||||
error.add("stack_trace", extractor::stackTrace);
|
||||
});
|
||||
});
|
||||
members.add("ecs.version", "8.11");
|
||||
members.add("tags", ILoggingEvent::getMarkerList)
|
||||
.whenNotNull()
|
||||
.as(ElasticCommonSchemaStructuredLogFormatter::getMarkers)
|
||||
.whenNotEmpty();
|
||||
members.add("ecs").usingMembers((ecs) -> ecs.add("version", "8.11"));
|
||||
}
|
||||
|
||||
private static Map<String, Object> nested(List<KeyValuePair> keyValuePairs) {
|
||||
return ElasticCommonSchemaPairs.nested((nested) -> keyValuePairs
|
||||
.forEach((keyValuePair) -> nested.accept(keyValuePair.key, keyValuePair.value)));
|
||||
}
|
||||
|
||||
private static Set<String> getMarkers(List<Marker> markers) {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2012-2025 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.structured;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility to help with writing ElasticCommonSchema pairs in their nested form.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class ElasticCommonSchemaPairs {
|
||||
|
||||
private ElasticCommonSchemaPairs() {
|
||||
}
|
||||
|
||||
public static Map<String, Object> nested(Map<String, String> map) {
|
||||
return nested(map::forEach);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K, V> Map<String, Object> nested(Consumer<BiConsumer<K, V>> nested) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
nested.accept((name, value) -> {
|
||||
List<String> nameParts = List.of(name.toString().split("\\."));
|
||||
Map<String, Object> destination = result;
|
||||
for (int i = 0; i < nameParts.size() - 1; i++) {
|
||||
Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>());
|
||||
if (!(existing instanceof Map)) {
|
||||
String common = nameParts.subList(0, i + 1).stream().collect(Collectors.joining("."));
|
||||
throw new IllegalStateException("Duplicate ECS pairs added under '%s'".formatted(common));
|
||||
}
|
||||
destination = (Map<String, Object>) existing;
|
||||
}
|
||||
Object previous = destination.put(nameParts.get(nameParts.size() - 1), value);
|
||||
Assert.state(previous == null, () -> "Duplicate ECS pairs added under '%s'".formatted(name));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
|
@ -80,10 +80,12 @@ public record ElasticCommonSchemaProperties(Service service) {
|
|||
static final Service NONE = new Service(null, null, null, null);
|
||||
|
||||
void jsonMembers(Members<?> members) {
|
||||
members.add("service.name", this::name).whenHasLength();
|
||||
members.add("service.version", this::version).whenHasLength();
|
||||
members.add("service.environment", this::environment).whenHasLength();
|
||||
members.add("service.node.name", this::nodeName).whenHasLength();
|
||||
members.add("service").usingMembers((service) -> {
|
||||
service.add("name", this::name).whenHasLength();
|
||||
service.add("version", this::version).whenHasLength();
|
||||
service.add("environment", this::environment).whenHasLength();
|
||||
service.add("node").usingMembers((node) -> node.add("name", this::nodeName).whenHasLength());
|
||||
});
|
||||
}
|
||||
|
||||
Service withDefaults(Environment environment) {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.logging.log4j2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -68,38 +69,51 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
String json = this.formatter.format(event);
|
||||
assertThat(json).endsWith("\n");
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
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", "message", "mdc-1", "mdc-v-1", "ecs.version", "8.11"));
|
||||
Map<String, Object> expected = new HashMap<>();
|
||||
expected.put("@timestamp", "2024-07-02T08:49:53Z");
|
||||
expected.put("message", "message");
|
||||
expected.put("mdc-1", "mdc-v-1");
|
||||
expected.put("ecs", Map.of("version", "8.11"));
|
||||
expected.put("process", map("pid", 1, "thread", map("name", "main")));
|
||||
expected.put("log", map("level", "INFO", "logger", "org.example.Test"));
|
||||
expected.put("service",
|
||||
map("name", "name", "version", "1.0.0", "environment", "test", "node", map("name", "node-1")));
|
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldFormatException() {
|
||||
MutableLogEvent event = createEvent();
|
||||
event.setThrown(new RuntimeException("Boom"));
|
||||
String json = this.formatter.format(event);
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
assertThat(deserialized)
|
||||
.containsAllEntriesOf(map("error.type", "java.lang.RuntimeException", "error.message", "Boom"));
|
||||
String stackTrace = (String) deserialized.get("error.stack_trace");
|
||||
assertThat(stackTrace).startsWith(
|
||||
"""
|
||||
java.lang.RuntimeException: Boom
|
||||
\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
|
||||
assertThat(json).contains(
|
||||
"""
|
||||
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
Map<String, Object> expectedError = new HashMap<>();
|
||||
expectedError.put("type", "java.lang.RuntimeException");
|
||||
expectedError.put("message", "Boom");
|
||||
assertThat(error).containsAllEntriesOf(expectedError);
|
||||
String stackTrace = (String) error.get("stack_trace");
|
||||
assertThat(stackTrace)
|
||||
.startsWith(String.format("java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.log4j2."
|
||||
+ "ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"));
|
||||
assertThat(json).contains(String
|
||||
.format("java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.log4j2."
|
||||
+ "ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldFormatExceptionUsingStackTracePrinter() {
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
|
||||
this.customizer);
|
||||
MutableLogEvent event = createEvent();
|
||||
event.setThrown(new RuntimeException("Boom"));
|
||||
Map<String, Object> deserialized = deserialize(this.formatter.format(event));
|
||||
String stackTrace = (String) deserialized.get("error.stack_trace");
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
String stackTrace = (String) error.get("stack_trace");
|
||||
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
|
||||
}
|
||||
|
||||
|
@ -111,10 +125,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
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"));
|
||||
assertThat(deserialized.get("message")).isEqualTo(expectedMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -131,11 +142,8 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
String json = this.formatter.format(event);
|
||||
assertThat(json).endsWith("\n");
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
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", "message", "ecs.version", "8.11", "tags",
|
||||
List.of("grandchild", "grandparent", "grandparent1", "parent", "parent1")));
|
||||
assertThat(deserialized.get("tags"))
|
||||
.isEqualTo(List.of("grandchild", "grandparent", "grandparent1", "parent", "parent1"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -62,12 +62,14 @@ class StructuredLogLayoutTests extends AbstractStructuredLoggingTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldSupportEcsCommonFormat() {
|
||||
StructuredLogLayout layout = newBuilder().setFormat("ecs").build();
|
||||
String json = layout.toSerializable(createEvent(new RuntimeException("Boom!")));
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
assertThat(deserialized).containsKey("ecs.version");
|
||||
assertThat(deserialized.get("error.stack_trace")).isEqualTo("stacktrace:RuntimeException");
|
||||
assertThat(deserialized).containsEntry("ecs", Map.of("version", "8.11"));
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
assertThat(error.get("stack_trace")).isEqualTo("stacktrace:RuntimeException");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.boot.logging.logback;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -72,33 +73,45 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
String json = this.formatter.format(event);
|
||||
assertThat(json).endsWith("\n");
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
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", "message", "mdc-1", "mdc-v-1", "kv-1", "kv-v-1", "ecs.version", "8.11"));
|
||||
Map<String, Object> expected = new HashMap<>();
|
||||
expected.put("@timestamp", "2024-07-02T08:49:53Z");
|
||||
expected.put("message", "message");
|
||||
expected.put("mdc-1", "mdc-v-1");
|
||||
expected.put("kv-1", "kv-v-1");
|
||||
expected.put("ecs", Map.of("version", "8.11"));
|
||||
expected.put("process", map("pid", 1, "thread", map("name", "main")));
|
||||
expected.put("log", map("level", "INFO", "logger", "org.example.Test"));
|
||||
expected.put("service",
|
||||
map("name", "name", "version", "1.0.0", "environment", "test", "node", map("name", "node-1")));
|
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldFormatException() {
|
||||
LoggingEvent event = createEvent();
|
||||
event.setMDCPropertyMap(Collections.emptyMap());
|
||||
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
|
||||
String json = this.formatter.format(event);
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
assertThat(deserialized)
|
||||
.containsAllEntriesOf(map("error.type", "java.lang.RuntimeException", "error.message", "Boom"));
|
||||
String stackTrace = (String) deserialized.get("error.stack_trace");
|
||||
assertThat(stackTrace).startsWith(
|
||||
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"
|
||||
.formatted());
|
||||
assertThat(json).contains(
|
||||
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"
|
||||
.formatted()
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r"));
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
Map<String, Object> expectedError = new HashMap<>();
|
||||
expectedError.put("type", "java.lang.RuntimeException");
|
||||
expectedError.put("message", "Boom");
|
||||
assertThat(error).containsAllEntriesOf(expectedError);
|
||||
String stackTrace = (String) error.get("stack_trace");
|
||||
assertThat(stackTrace)
|
||||
.startsWith(String.format("java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback."
|
||||
+ "ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"));
|
||||
assertThat(json).contains(String
|
||||
.format("java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback."
|
||||
+ "ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldFormatExceptionUsingStackTracePrinter() {
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
|
||||
getThrowableProxyConverter(), this.customizer);
|
||||
|
@ -106,7 +119,8 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
event.setMDCPropertyMap(Collections.emptyMap());
|
||||
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
|
||||
Map<String, Object> deserialized = deserialize(this.formatter.format(event));
|
||||
String stackTrace = (String) deserialized.get("error.stack_trace");
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
String stackTrace = (String) error.get("stack_trace");
|
||||
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
|
||||
}
|
||||
|
||||
|
@ -123,13 +137,19 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
|||
grandparent.add(parent1);
|
||||
event.addMarker(grandparent);
|
||||
String json = this.formatter.format(event);
|
||||
assertThat(json).endsWith("\n");
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
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", "message",
|
||||
"ecs.version", "8.11", "tags", List.of("child", "child1", "grandparent", "parent", "parent1")));
|
||||
assertThat(deserialized.get("tags")).isEqualTo(List.of("child", "child1", "grandparent", "parent", "parent1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNestMdcAndKeyValuePairs() {
|
||||
LoggingEvent event = createEvent();
|
||||
event.setMDCPropertyMap(Map.of("a1.b1.c1", "A1B1C1", "a1.b1.c2", "A1B1C2"));
|
||||
event.setKeyValuePairs(keyValuePairs("a2.b1.c1", "A2B1C1", "a2.b1.c2", "A2B1C2"));
|
||||
String json = this.formatter.format(event);
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
assertThat(deserialized.get("a1")).isEqualTo(Map.of("b1", Map.of("c1", "A1B1C1", "c2", "A1B1C2")));
|
||||
assertThat(deserialized.get("a2")).isEqualTo(Map.of("b1", Map.of("c1", "A2B1C1", "c2", "A2B1C2")));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ class StructuredLogEncoderTests extends AbstractStructuredLoggingTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldSupportEcsCommonFormat() {
|
||||
this.encoder.setFormat("ecs");
|
||||
this.encoder.start();
|
||||
|
@ -79,8 +80,9 @@ class StructuredLogEncoderTests extends AbstractStructuredLoggingTests {
|
|||
event.setMDCPropertyMap(Collections.emptyMap());
|
||||
String json = encode(event);
|
||||
Map<String, Object> deserialized = deserialize(json);
|
||||
assertThat(deserialized).containsKey("ecs.version");
|
||||
assertThat(deserialized.get("error.stack_trace")).isEqualTo("stacktrace:RuntimeException");
|
||||
assertThat(deserialized).containsEntry("ecs", Map.of("version", "8.11"));
|
||||
Map<String, Object> error = (Map<String, Object>) deserialized.get("error");
|
||||
assertThat(error.get("stack_trace")).isEqualTo("stacktrace:RuntimeException");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2012-2025 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.structured;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ElasticCommonSchemaPairs}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ElasticCommonSchemaPairsTests {
|
||||
|
||||
@Test
|
||||
void nestedExpandsNames() {
|
||||
Map<String, String> map = Map.of("a1.b1.c1", "A1B1C1", "a1.b2.c1", "A1B2C1", "a1.b1.c2", "A1B1C2");
|
||||
Map<String, Object> expected = new LinkedHashMap<>();
|
||||
Map<String, Object> a1 = new LinkedHashMap<>();
|
||||
Map<String, Object> b1 = new LinkedHashMap<>();
|
||||
Map<String, Object> b2 = new LinkedHashMap<>();
|
||||
expected.put("a1", a1);
|
||||
a1.put("b1", b1);
|
||||
a1.put("b2", b2);
|
||||
b1.put("c1", "A1B1C1");
|
||||
b1.put("c2", "A1B1C2");
|
||||
b2.put("c1", "A1B2C1");
|
||||
assertThat(ElasticCommonSchemaPairs.nested(map)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedWhenDuplicateInParentThrowsException() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
map.put("a1.b1.c1", "A1B1C1");
|
||||
map.put("a1.b1", "A1B1");
|
||||
assertThatIllegalStateException().isThrownBy(() -> ElasticCommonSchemaPairs.nested(map))
|
||||
.withMessage("Duplicate ECS pairs added under 'a1.b1'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedWhenDuplicateInLeafThrowsException() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
map.put("a1.b1", "A1B1");
|
||||
map.put("a1.b1.c1", "A1B1C1");
|
||||
assertThatIllegalStateException().isThrownBy(() -> ElasticCommonSchemaPairs.nested(map))
|
||||
.withMessage("Duplicate ECS pairs added under 'a1.b1'");
|
||||
}
|
||||
|
||||
}
|
|
@ -77,9 +77,17 @@ class ElasticCommonSchemaPropertiesTests {
|
|||
ElasticCommonSchemaProperties properties = new ElasticCommonSchemaProperties(
|
||||
new Service("spring", "1.2.3", "prod", "boot"));
|
||||
JsonWriter<ElasticCommonSchemaProperties> writer = JsonWriter.of(properties::jsonMembers);
|
||||
assertThat(writer.writeToString(properties))
|
||||
.isEqualTo("{\"service.name\":\"spring\",\"service.version\":\"1.2.3\","
|
||||
+ "\"service.environment\":\"prod\",\"service.node.name\":\"boot\"}");
|
||||
assertThat(writer.writeToString(properties)).isEqualToNormalizingNewlines("""
|
||||
{
|
||||
"service": {
|
||||
"name": "spring",
|
||||
"version": "1.2.3",
|
||||
"environment": "prod",
|
||||
"node": {
|
||||
"name": "boot"
|
||||
}
|
||||
}
|
||||
}""".replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -54,7 +54,7 @@ class SampleStructuredLoggingApplicationTests {
|
|||
void json(CapturedOutput output) {
|
||||
SampleStructuredLoggingApplication.main(new String[0]);
|
||||
assertThat(output).doesNotContain("{\"@timestamp\"")
|
||||
.contains("\"process.thread.name\":\"!!")
|
||||
.contains("\"thread\":{\"name\":\"!!")
|
||||
.contains("\"process.procid\"")
|
||||
.contains("\"message\":\"Starting SampleStructuredLoggingApplication")
|
||||
.contains("\"foo\":\"hello");
|
||||
|
|
Loading…
Reference in New Issue