diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/json/JsonFormat.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/json/JsonFormat.java index 0a4dade48c9c..2c3cf6f886df 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/json/JsonFormat.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/json/JsonFormat.java @@ -25,7 +25,7 @@ */ public interface JsonFormat { - default void setPid(long pid) { + default void setPid(Long pid) { } default void setServiceName(String serviceName) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CommonJsonFormats.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CommonJsonFormats.java new file mode 100644 index 000000000000..c9ff13d279b8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CommonJsonFormats.java @@ -0,0 +1,210 @@ +/* + * 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.logback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Supplier; + +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import org.slf4j.event.KeyValuePair; + +import org.springframework.boot.logging.json.Field; +import org.springframework.boot.logging.json.Key; +import org.springframework.boot.logging.json.Value; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Common JSON formats. + * + * @author Moritz Halbritter + */ +final class CommonJsonFormats { + + private static final Map> FORMATS = Map.of("ecs", CommonJsonFormats::ecs); + + private CommonJsonFormats() { + } + + static LogbackJsonFormat ecs() { + return new EcsJsonFormat(); + } + + static Set names() { + return FORMATS.keySet(); + } + + /** + * Returns a new instance of the requested {@link LogbackJsonFormat}. Returns + * {@code null} if the format isn't known. + * @param format the requested format + * @return a new instance of the requested format or{@code null} if the format isn't + * known. + */ + static LogbackJsonFormat create(String format) { + Assert.notNull(format, "Format must not be null"); + Supplier factory = FORMATS.get(format.toLowerCase(Locale.ENGLISH)); + if (factory == null) { + return null; + } + return factory.get(); + } + + abstract static class BaseLogbackJsonFormat implements LogbackJsonFormat { + + private Long pid; + + private String serviceName; + + private String serviceVersion; + + private String serviceNodeName; + + private String serviceEnvironment; + + private ThrowableProxyConverter throwableProxyConverter; + + @Override + public void setPid(Long pid) { + this.pid = pid; + } + + @Override + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + @Override + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } + + @Override + public void setThrowableProxyConverter(ThrowableProxyConverter throwableProxyConverter) { + this.throwableProxyConverter = throwableProxyConverter; + } + + @Override + public void setServiceNodeName(String serviceNodeName) { + this.serviceNodeName = serviceNodeName; + } + + @Override + public void setServiceEnvironment(String serviceEnvironment) { + this.serviceEnvironment = serviceEnvironment; + } + + Long getPid() { + return this.pid; + } + + String getServiceName() { + return this.serviceName; + } + + String getServiceVersion() { + return this.serviceVersion; + } + + ThrowableProxyConverter getThrowableProxyConverter() { + return this.throwableProxyConverter; + } + + String getServiceNodeName() { + return this.serviceNodeName; + } + + String getServiceEnvironment() { + return this.serviceEnvironment; + } + + } + + /** + * ECS logging + * format. + */ + private static final class EcsJsonFormat extends BaseLogbackJsonFormat { + + @Override + public Iterable getFields(ILoggingEvent event) { + List fields = new ArrayList<>(); + fields.add(Field.of(Key.verbatim("@timestamp"), Value.verbatim(event.getInstant().toString()))); + fields.add(Field.of(Key.verbatim("log.level"), Value.verbatim(event.getLevel().toString()))); + if (getPid() != null) { + fields.add(Field.of(Key.verbatim("process.pid"), Value.of(getPid()))); + } + fields.add(Field.of(Key.verbatim("process.thread.name"), Value.escaped(event.getThreadName()))); + if (getServiceName() != null) { + fields.add(Field.of(Key.verbatim("service.name"), Value.escaped(getServiceName()))); + } + if (getServiceVersion() != null) { + fields.add(Field.of(Key.verbatim("service.version"), Value.escaped(getServiceVersion()))); + } + if (getServiceEnvironment() != null) { + fields.add(Field.of(Key.verbatim("service.environment"), Value.escaped(getServiceEnvironment()))); + } + if (getServiceNodeName() != null) { + fields.add(Field.of(Key.verbatim("service.node.name"), Value.escaped(getServiceNodeName()))); + } + fields.add(Field.of(Key.verbatim("log.logger"), Value.escaped(event.getLoggerName()))); + fields.add(Field.of(Key.verbatim("message"), Value.escaped(event.getFormattedMessage()))); + addMdc(event, fields); + addKeyValuePairs(event, fields); + IThrowableProxy throwable = event.getThrowableProxy(); + if (throwable != null) { + fields.add(Field.of(Key.verbatim("error.type"), Value.verbatim(throwable.getClassName()))); + fields.add(Field.of(Key.verbatim("error.message"), Value.escaped(throwable.getMessage()))); + fields.add(Field.of(Key.verbatim("error.stack_trace"), + Value.escaped(getThrowableProxyConverter().convert(event)))); + } + fields.add(Field.of(Key.verbatim("ecs.version"), Value.verbatim("8.11"))); + return fields; + } + + private void addKeyValuePairs(ILoggingEvent event, List fields) { + List keyValuePairs = event.getKeyValuePairs(); + if (CollectionUtils.isEmpty(keyValuePairs)) { + return; + } + for (KeyValuePair keyValuePair : keyValuePairs) { + fields.add(Field.of(Key.escaped(keyValuePair.key), + Value.escaped(ObjectUtils.nullSafeToString(keyValuePair.value)))); + } + } + + private static void addMdc(ILoggingEvent event, List fields) { + Map mdc = event.getMDCPropertyMap(); + if (CollectionUtils.isEmpty(mdc)) { + return; + } + for (Entry entry : mdc.entrySet()) { + fields.add(Field.of(Key.escaped(entry.getKey()), Value.escaped(entry.getValue()))); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/JsonEncoder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/JsonEncoder.java index ed2c272d3ec1..0b8440085880 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/JsonEncoder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/JsonEncoder.java @@ -17,29 +17,16 @@ package org.springframework.boot.logging.logback; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Supplier; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; -import org.slf4j.event.KeyValuePair; import org.springframework.beans.BeanUtils; import org.springframework.boot.logging.json.Field; -import org.springframework.boot.logging.json.JsonFormat; -import org.springframework.boot.logging.json.Key; -import org.springframework.boot.logging.json.Value; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; /** * {@link Encoder Logback encoder} which encodes to JSON-based formats. @@ -127,9 +114,7 @@ public void start() { Assert.state(this.format != null, "Format has not been set"); super.start(); this.throwableProxyConverter.start(); - if (this.pid != null) { - this.format.setPid(this.pid); - } + this.format.setPid(this.pid); this.format.setServiceName(this.serviceName); this.format.setServiceVersion(this.serviceVersion); this.format.setServiceEnvironment(this.serviceEnvironment); @@ -175,174 +160,4 @@ public byte[] footerBytes() { return null; } - public interface LogbackJsonFormat extends JsonFormat { - - default void setThrowableProxyConverter(ThrowableProxyConverter throwableProxyConverter) { - } - - } - - abstract static class BaseLogbackJsonFormat implements LogbackJsonFormat { - - private Long pid = null; - - private String serviceName; - - private String serviceVersion; - - private String serviceNodeName; - - private String serviceEnvironment; - - private ThrowableProxyConverter throwableProxyConverter; - - @Override - public void setPid(long pid) { - this.pid = pid; - } - - @Override - public void setServiceName(String serviceName) { - this.serviceName = serviceName; - } - - @Override - public void setServiceVersion(String serviceVersion) { - this.serviceVersion = serviceVersion; - } - - @Override - public void setThrowableProxyConverter(ThrowableProxyConverter throwableProxyConverter) { - this.throwableProxyConverter = throwableProxyConverter; - } - - @Override - public void setServiceNodeName(String serviceNodeName) { - this.serviceNodeName = serviceNodeName; - } - - @Override - public void setServiceEnvironment(String serviceEnvironment) { - this.serviceEnvironment = serviceEnvironment; - } - - Long getPid() { - return this.pid; - } - - String getServiceName() { - return this.serviceName; - } - - String getServiceVersion() { - return this.serviceVersion; - } - - ThrowableProxyConverter getThrowableProxyConverter() { - return this.throwableProxyConverter; - } - - String getServiceNodeName() { - return this.serviceNodeName; - } - - String getServiceEnvironment() { - return this.serviceEnvironment; - } - - } - - static final class CommonJsonFormats { - - private static final Map> FORMATS = Map.of("ecs", CommonJsonFormats::ecs); - - static EcsJsonFormat ecs() { - return new EcsJsonFormat(); - } - - static Set names() { - return FORMATS.keySet(); - } - - /** - * Returns a new instance of the requested {@link LogbackJsonFormat}. Returns - * {@code null} if the format isn't known. - * @param format the requested format - * @return a new instance of the request format or{@code null} if the format isn't - * known. - */ - static LogbackJsonFormat create(String format) { - Assert.notNull(format, "Format must not be null"); - Supplier factory = FORMATS.get(format.toLowerCase()); - if (factory == null) { - return null; - } - return factory.get(); - } - - private static final class EcsJsonFormat extends BaseLogbackJsonFormat { - - @Override - public Iterable getFields(ILoggingEvent event) { - List fields = new ArrayList<>(); - fields.add(Field.of(Key.verbatim("@timestamp"), Value.verbatim(event.getInstant().toString()))); - fields.add(Field.of(Key.verbatim("log.level"), Value.verbatim(event.getLevel().toString()))); - if (getPid() != null) { - fields.add(Field.of(Key.verbatim("process.pid"), Value.of(getPid()))); - } - fields.add(Field.of(Key.verbatim("process.thread.name"), Value.escaped(event.getThreadName()))); - if (getServiceName() != null) { - fields.add(Field.of(Key.verbatim("service.name"), Value.escaped(getServiceName()))); - } - if (getServiceVersion() != null) { - fields.add(Field.of(Key.verbatim("service.version"), Value.escaped(getServiceVersion()))); - } - if (getServiceEnvironment() != null) { - fields.add(Field.of(Key.verbatim("service.environment"), Value.escaped(getServiceEnvironment()))); - } - if (getServiceNodeName() != null) { - fields.add(Field.of(Key.verbatim("service.node.name"), Value.escaped(getServiceNodeName()))); - } - fields.add(Field.of(Key.verbatim("log.logger"), Value.escaped(event.getLoggerName()))); - fields.add(Field.of(Key.verbatim("message"), Value.escaped(event.getFormattedMessage()))); - addMdc(event, fields); - addKeyValuePairs(event, fields); - IThrowableProxy throwable = event.getThrowableProxy(); - if (throwable != null) { - fields.add(Field.of(Key.verbatim("error.type"), Value.verbatim(throwable.getClassName()))); - fields.add(Field.of(Key.verbatim("error.message"), Value.escaped(throwable.getMessage()))); - fields.add(Field.of(Key.verbatim("error.stack_trace"), - Value.escaped(getThrowableProxyConverter().convert(event)))); - } - fields.add(Field.of(Key.verbatim("ecs.version"), Value.verbatim("8.11"))); - return fields; - // TODO: service env, service node name, - // event dataset - } - - private void addKeyValuePairs(ILoggingEvent event, List fields) { - List keyValuePairs = event.getKeyValuePairs(); - if (CollectionUtils.isEmpty(keyValuePairs)) { - return; - } - for (KeyValuePair keyValuePair : keyValuePairs) { - fields.add(Field.of(Key.escaped(keyValuePair.key), - Value.escaped(ObjectUtils.nullSafeToString(keyValuePair.value)))); - } - } - - private static void addMdc(ILoggingEvent event, List fields) { - Map mdc = event.getMDCPropertyMap(); - if (CollectionUtils.isEmpty(mdc)) { - return; - } - for (Entry entry : mdc.entrySet()) { - fields.add(Field.of(Key.escaped(entry.getKey()), Value.escaped(entry.getValue()))); - } - } - - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackJsonFormat.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackJsonFormat.java new file mode 100644 index 000000000000..8925768e5f3a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackJsonFormat.java @@ -0,0 +1,35 @@ +/* + * 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.logback; + +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import org.springframework.boot.logging.json.JsonFormat; + +/** + * Specialization of {@link JsonFormat} for Logback. + * + * @author Moritz Halbritter + * @since 3.4.0 + */ +public interface LogbackJsonFormat extends JsonFormat { + + default void setThrowableProxyConverter(ThrowableProxyConverter throwableProxyConverter) { + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/JsonEncoderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/JsonEncoderTests.java index 9eaa080abd0b..f4d57c001608 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/JsonEncoderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/JsonEncoderTests.java @@ -32,8 +32,6 @@ import org.junit.jupiter.api.Test; import org.slf4j.event.KeyValuePair; -import org.springframework.boot.logging.logback.JsonEncoder.CommonJsonFormats; - import static org.assertj.core.api.Assertions.assertThat; /** diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/CustomJsonFormat.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/CustomJsonFormat.java index 65f16c7d8579..62810ce52399 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/CustomJsonFormat.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/CustomJsonFormat.java @@ -23,7 +23,7 @@ import org.springframework.boot.logging.json.Field; import org.springframework.boot.logging.json.Key; import org.springframework.boot.logging.json.Value; -import org.springframework.boot.logging.logback.JsonEncoder.LogbackJsonFormat; +import org.springframework.boot.logging.logback.LogbackJsonFormat; /** * A custom implementation of a JSON format. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties index 89934cf616f8..c08aca59d479 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties @@ -2,5 +2,5 @@ spring.application.name=simple test.name=Phil sample.name=Andy # TODO MH: Remove -# logging.json.console=ecs -# logging.json.console=smoketest.simple.CustomJsonFormat +#logging.json.console=ecs +logging.json.console=smoketest.simple.CustomJsonFormat diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/logback-spring.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/logback-spring.xml.disabled similarity index 100% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/logback-spring.xml rename to spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/logback-spring.xml.disabled