diff --git a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java index 20312b38..6cf400aa 100644 --- a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java +++ b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java @@ -142,19 +142,23 @@ public static void serializeOrigin(StringBuilder builder, StackTraceElement stac } public static void serializeOrigin(StringBuilder builder, String fileName, String methodName, int lineNumber) { - builder.append("\"log.origin\":{"); - builder.append("\"file.name\":\""); + builder.append("\"log\":{"); + builder.append("\"origin\":{"); + builder.append("\"file\":{"); + builder.append("\"name\":\""); JsonUtils.quoteAsString(fileName, builder); - builder.append("\","); - builder.append("\"function\":\""); - JsonUtils.quoteAsString(methodName, builder); builder.append('"'); if (lineNumber >= 0) { builder.append(','); - builder.append("\"file.line\":"); + builder.append("\"line\":"); builder.append(lineNumber); } builder.append("},"); + builder.append("\"function\":\""); + JsonUtils.quoteAsString(methodName, builder); + builder.append('"'); + builder.append("}"); + builder.append("},"); } public static void serializeMDC(StringBuilder builder, Map properties) { diff --git a/ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java b/ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java index b5b53c86..653c5073 100644 --- a/ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java +++ b/ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java @@ -37,8 +37,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -92,20 +90,30 @@ void validateLog(JsonNode logLine) { String specFieldName = specField.getKey(); JsonNode specForField = specField.getValue(); JsonNode fieldInLog = logLine.get(specFieldName); + if (fieldInLog == null) { + fieldInLog = logLine.at("/" + specFieldName.replace('.', '/')); + } - validateRequiredField(logLine, specFieldName, specForField.get("required").booleanValue()); - if (fieldInLog != null) { + validateRequiredField(logLine, specFieldName, fieldInLog, specForField.get("required").booleanValue()); + if (!fieldInLog.isMissingNode()) { validateIndex(logLine, logFieldNames, specFieldName, specForField.get("index")); validateType(fieldInLog, specForField.get("type").textValue()); + validateNesting(logLine, specFieldName, specForField.has("top_level_field") && specForField.get("top_level_field").asBoolean(false)); } } } - private void validateRequiredField(JsonNode logLine, String specFieldName, boolean required) { + private void validateNesting(JsonNode logLine, String specFieldName, boolean topLevelField) { + if (topLevelField) { + assertThat(logLine.at("/" + specFieldName.replace('.', '/')).isMissingNode()).isTrue(); + } + } + + private void validateRequiredField(JsonNode logLine, String specFieldName, JsonNode fieldInLog, boolean required) { if (required) { - assertThat(logLine.get(specFieldName)) - .describedAs(logLine.toString()) - .isNotNull(); + assertThat(fieldInLog.isMissingNode()) + .describedAs("Expected %s to be non-null: %s", specFieldName, logLine) + .isFalse(); } } @@ -196,9 +204,9 @@ void testLogExceptionNullMessage() throws Exception { @Test void testLogOrigin() throws Exception { debug("test"); - assertThat(getAndValidateLastLogLine().get("log.origin").get("file.name").textValue()).endsWith(".java"); - assertThat(getAndValidateLastLogLine().get("log.origin").get("function").textValue()).isEqualTo("debug"); - assertThat(getAndValidateLastLogLine().get("log.origin").get("file.line").intValue()).isPositive(); + assertThat(getAndValidateLastLogLine().at("/log/origin/file/name").textValue()).endsWith(".java"); + assertThat(getAndValidateLastLogLine().at("/log/origin/function").textValue()).isEqualTo("debug"); + assertThat(getAndValidateLastLogLine().at("/log/origin/file/line").intValue()).isPositive(); } public boolean putMdc(String key, String value) { diff --git a/ecs-logging-core/src/test/resources/spec/spec.json b/ecs-logging-core/src/test/resources/spec/spec.json index d1b88d42..6fc077da 100644 --- a/ecs-logging-core/src/test/resources/spec/spec.json +++ b/ecs-logging-core/src/test/resources/spec/spec.json @@ -15,8 +15,15 @@ "type": "string", "required": true, "index": 1, - "nesting_allowed": false, - "url": "https://www.elastic.co/guide/en/ecs/current/ecs-log.html" + "top_level_field": true, + "url": "https://www.elastic.co/guide/en/ecs/current/ecs-log.html", + "comment": [ + "This field SHOULD NOT be a nested object field but at the top level with a dot in the property name.", + "This is to make the JSON logs more human-readable.", + "Loggers MAY indent the log level so that the `message` field always starts at the exact same offset,", + "no matter the number of characters the log level has.", + "For example: `'DEBUG'` (5 chars) will not be indented, whereas ` 'WARN'` (4 chars) will be indented by one space character." + ] }, "message": { "type": "string", diff --git a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java index bdf91894..464a1f6f 100644 --- a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java +++ b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/EcsFormatterTest.java @@ -24,16 +24,16 @@ */ package co.elastic.logging.jul; -import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.logging.Level; import java.util.logging.LogRecord; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; public class EcsFormatterTest { @@ -57,8 +57,8 @@ public void testFormatWithIncludeOriginFlag() throws Exception { final String result = formatter.format(record); - assertThat(objectMapper.readTree(result).get("log.origin").get("file.name").textValue()).isEqualTo("ExampleClass.java"); - assertThat(objectMapper.readTree(result).get("log.origin").get("function").textValue()).isEqualTo("exampleMethod"); + assertThat(objectMapper.readTree(result).at("/log/origin/file/name").textValue()).isEqualTo("ExampleClass.java"); + assertThat(objectMapper.readTree(result).at("/log/origin/function").textValue()).isEqualTo("exampleMethod"); } @Test @@ -91,8 +91,8 @@ public void testFormatWithInnerClassName() throws Exception { record.setSourceClassName("test.ExampleClass$InnerClass"); JsonNode result = objectMapper.readTree(formatter.format(record)); - assertThat(result.get("log.origin").get("file.name").textValue()).isEqualTo("ExampleClass.java"); - assertThat(result.get("log.origin").get("function").textValue()).isEqualTo("exampleMethod"); + assertThat(result.at("/log/origin/file/name").textValue()).isEqualTo("ExampleClass.java"); + assertThat(result.at("/log/origin/function").textValue()).isEqualTo("exampleMethod"); } @Test @@ -101,8 +101,8 @@ public void testFormatWithInvalidClassName() throws Exception { record.setSourceClassName("$test.ExampleClass"); JsonNode result = objectMapper.readTree(formatter.format(record)); - assertThat(result.get("log.origin").get("file.name").textValue()).isEqualTo(""); - assertThat(result.get("log.origin").get("function").textValue()).isEqualTo("exampleMethod"); + assertThat(result.at("/log/origin/file/name").textValue()).isEqualTo(""); + assertThat(result.at("/log/origin/function").textValue()).isEqualTo("exampleMethod"); } } diff --git a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/JulLoggingTest.java b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/JulLoggingTest.java index 50630fa1..37887492 100644 --- a/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/JulLoggingTest.java +++ b/jul-ecs-formatter/src/test/java/co/elastic/logging/jul/JulLoggingTest.java @@ -40,8 +40,6 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.StreamHandler; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static org.assertj.core.api.Assertions.assertThat; @@ -127,8 +125,8 @@ void setUp() { @Test void testLogOrigin() throws Exception { debug("test"); - assertThat(getAndValidateLastLogLine().get("log.origin").get("file.name").textValue()).endsWith(".java"); - assertThat(getAndValidateLastLogLine().get("log.origin").get("function").textValue()).isEqualTo("debug"); + assertThat(getAndValidateLastLogLine().at("/log/origin/file/name").textValue()).endsWith(".java"); + assertThat(getAndValidateLastLogLine().at("/log/origin/function").textValue()).isEqualTo("debug"); //No file.line for JUL }