diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListener.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListener.java index ec1cd2159..7e242e83a 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListener.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListener.java @@ -32,8 +32,6 @@ /** * Parse SOFATracer Configuration in early stage. * - * @author qilong.zql - * @author huzijie * @since 2.2.2 */ public class SofaTracerConfigurationListener @@ -98,7 +96,23 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String.valueOf(sofaTracerProperties.getSamplerPercentage())); SofaTracerConfiguration.setProperty(SofaTracerConfiguration.JSON_FORMAT_OUTPUT, - String.valueOf(sofaTracerProperties.isJsonOutput())); + String.valueOf(resolveJsonOutput(environment, sofaTracerProperties))); + } + + private boolean resolveJsonOutput(ConfigurableEnvironment environment, + SofaTracerProperties sofaTracerProperties) { + if (Binder.get(environment).bind("sofa.boot.tracer.json-output", Boolean.class).isBound()) { + return sofaTracerProperties.isJsonOutput(); + } + if (sofaTracerProperties.isOutputStructured()) { + return isStructuredLoggingEnabled(environment); + } + return sofaTracerProperties.isJsonOutput(); + } + + private boolean isStructuredLoggingEnabled(ConfigurableEnvironment environment) { + return StringUtils.hasText(environment.getProperty("logging.structured.format.console")) + || StringUtils.hasText(environment.getProperty("logging.structured.format.file")); } @Override diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerProperties.java b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerProperties.java index 413da6327..5a9b01df1 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerProperties.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/main/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerProperties.java @@ -29,8 +29,6 @@ /** * Configuration properties to configure tracer. * - * @author yangguanchao - * @author huzijie * @since 2018/04/30 */ @ConfigurationProperties("sofa.boot.tracer") @@ -93,6 +91,11 @@ public class SofaTracerProperties { */ private boolean jsonOutput = true; + /** + * Follow SOFABoot structured logging when jsonOutput is not explicitly configured. + */ + private boolean outputStructured; + public String getDisableDigestLog() { return disableDigestLog; } @@ -180,4 +183,12 @@ public boolean isJsonOutput() { public void setJsonOutput(boolean jsonOutput) { this.jsonOutput = jsonOutput; } + + public boolean isOutputStructured() { + return outputStructured; + } + + public void setOutputStructured(boolean outputStructured) { + this.outputStructured = outputStructured; + } } diff --git a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListenerTests.java b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListenerTests.java index 0c621487c..4962c4295 100644 --- a/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListenerTests.java +++ b/sofa-boot-project/sofa-boot-autoconfigure/src/test/java/com/alipay/sofa/boot/autoconfigure/tracer/SofaTracerConfigurationListenerTests.java @@ -37,9 +37,6 @@ /** * Tests for {@link SofaTracerConfigurationListener}. - * - * @author huzijie - * @version SofaTracerConfigurationListenerTests.java, v 0.1 2023年01月11日 3:28 PM huzijie Exp $ */ public class SofaTracerConfigurationListenerTests { @@ -86,6 +83,8 @@ public void injectSofaTracerConfiguration() { props.put("sofa.boot.tracer.samplerName", "test"); props.put("sofa.boot.tracer.samplerPercentage", "200.0"); props.put("sofa.boot.tracer.samplerCustomRuleClassName", "TestRuleClass"); + props.put("sofa.boot.tracer.output-structured", "true"); + props.put("logging.structured.format.file", "ecs"); props.put("sofa.boot.tracer.jsonOutput", "false"); application.setDefaultProperties(props); application.addListeners(new SpringCloudConfigListener()); @@ -129,6 +128,39 @@ public void injectSofaTracerConfiguration() { .isEqualTo("false"); } + @Test + public void tracerJsonOutputFollowsStructuredLogging() { + SpringApplication application = new SpringApplication(Config.class); + application.setWebApplicationType(WebApplicationType.NONE); + Map props = new HashMap<>(); + props.put("spring.application.name", "tracer-structured"); + props.put("sofa.boot.tracer.output-structured", "true"); + props.put("logging.structured.format.console", "logstash"); + application.setDefaultProperties(props); + application.addListeners(new SpringCloudConfigListener()); + application.addListeners(new SofaTracerConfigurationListener()); + this.context = application.run(); + + assertThat(SofaTracerConfiguration.getProperty(SofaTracerConfiguration.JSON_FORMAT_OUTPUT)) + .isEqualTo("true"); + } + + @Test + public void tracerJsonOutputCanBeDisabledWhenStructuredLoggingIsDisabled() { + SpringApplication application = new SpringApplication(Config.class); + application.setWebApplicationType(WebApplicationType.NONE); + Map props = new HashMap<>(); + props.put("spring.application.name", "tracer-pattern"); + props.put("sofa.boot.tracer.output-structured", "true"); + application.setDefaultProperties(props); + application.addListeners(new SpringCloudConfigListener()); + application.addListeners(new SofaTracerConfigurationListener()); + this.context = application.run(); + + assertThat(SofaTracerConfiguration.getProperty(SofaTracerConfiguration.JSON_FORMAT_OUTPUT)) + .isEqualTo("false"); + } + @Configuration(proxyBeanMethods = false) static class Config { diff --git a/sofa-boot-project/sofa-boot-core/rpc-sofa-boot/src/main/resources/com/alipay/sofa/rpc/boot/log/logback/log-conf.xml b/sofa-boot-project/sofa-boot-core/rpc-sofa-boot/src/main/resources/com/alipay/sofa/rpc/boot/log/logback/log-conf.xml index b8d541a90..6f482db53 100644 --- a/sofa-boot-project/sofa-boot-core/rpc-sofa-boot/src/main/resources/com/alipay/sofa/rpc/boot/log/logback/log-conf.xml +++ b/sofa-boot-project/sofa-boot-core/rpc-sofa-boot/src/main/resources/com/alipay/sofa/rpc/boot/log/logback/log-conf.xml @@ -20,8 +20,9 @@ 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -42,8 +43,9 @@ 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -65,8 +67,9 @@ 30 - + %d %-5p %-32t %c{2} - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -87,8 +90,9 @@ 30 - + %d %-5p %-32t %c{2} - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -109,8 +113,9 @@ 30 - + %d %-5p %-32t %c{2} - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -139,4 +144,4 @@ - \ No newline at end of file + diff --git a/sofa-boot-project/sofa-boot/pom.xml b/sofa-boot-project/sofa-boot/pom.xml index 505c3df7a..702ec3be7 100644 --- a/sofa-boot-project/sofa-boot/pom.xml +++ b/sofa-boot-project/sofa-boot/pom.xml @@ -36,14 +36,14 @@ - org.springframework.boot - spring-boot-starter-test - test + ch.qos.logback + logback-classic + true - ch.qos.logback - logback-classic + org.springframework.boot + spring-boot-starter-test test diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoder.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoder.java new file mode 100644 index 000000000..3249fce93 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoder.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alipay.sofa.boot.logging.logback; + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.encoder.Encoder; +import ch.qos.logback.core.encoder.EncoderBase; +import org.springframework.boot.logging.logback.StructuredLogEncoder; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Logback encoder that keeps SOFABoot's pattern output by default and switches to + * Spring Boot's structured logging encoder when a structured format system property is set. + */ +public class SofaBootStructuredLogEncoder extends EncoderBase { + + private String pattern; + + private Charset charset = StandardCharsets.UTF_8; + + private String formatProperty; + + private Encoder delegate; + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + public void setFormatProperty(String formatProperty) { + this.formatProperty = formatProperty; + } + + @Override + public void start() { + Assert.state(StringUtils.hasText(this.pattern), "Pattern has not been set"); + Assert.state(StringUtils.hasText(this.formatProperty), "Format property has not been set"); + this.delegate = createDelegate(); + super.start(); + } + + private Encoder createDelegate() { + String format = System.getProperty(this.formatProperty); + if (StringUtils.hasText(format)) { + return createStructuredDelegate(format); + } + return createPatternDelegate(); + } + + private Encoder createStructuredDelegate(String format) { + StructuredLogEncoder encoder = new StructuredLogEncoder(); + encoder.setContext(getContext()); + encoder.setCharset((this.charset != null) ? this.charset : StandardCharsets.UTF_8); + encoder.setFormat(format); + try { + encoder.start(); + return encoder; + } catch (RuntimeException exception) { + addWarn( + "Falling back to pattern logging because structured logging could not be initialized for " + + this.formatProperty, exception); + encoder.stop(); + return createPatternDelegate(); + } + } + + private Encoder createPatternDelegate() { + PatternLayoutEncoder encoder = new PatternLayoutEncoder(); + encoder.setContext(getContext()); + encoder.setPattern(this.pattern); + encoder.setCharset((this.charset != null) ? this.charset : StandardCharsets.UTF_8); + encoder.start(); + return encoder; + } + + @Override + public void stop() { + if (this.delegate != null) { + this.delegate.stop(); + } + super.stop(); + } + + @Override + public byte[] headerBytes() { + return (this.delegate != null) ? this.delegate.headerBytes() : null; + } + + @Override + public byte[] encode(ILoggingEvent event) { + Assert.state(this.delegate != null, "Encoder has not been started"); + return this.delegate.encode(event); + } + + @Override + public byte[] footerBytes() { + return (this.delegate != null) ? this.delegate.footerBytes() : null; + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/resources/sofa-boot/log/logback/log-conf.xml b/sofa-boot-project/sofa-boot/src/main/resources/sofa-boot/log/logback/log-conf.xml index 11057c726..dfdc12e6a 100644 --- a/sofa-boot-project/sofa-boot/src/main/resources/sofa-boot/log/logback/log-conf.xml +++ b/sofa-boot-project/sofa-boot/src/main/resources/sofa-boot/log/logback/log-conf.xml @@ -27,8 +27,9 @@ ${logging.path}/sofa-runtime/common-error.log.%d{yyyy-MM-dd} 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -43,8 +44,9 @@ ${logging.path}/sofa-runtime/sofa-default.log.%d{yyyy-MM-dd} 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -66,8 +68,9 @@ 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} @@ -88,8 +91,9 @@ 30 - + %d %-5p %-32t - %m%n + FILE_LOG_STRUCTURED_FORMAT ${file.encoding} diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/LogEnvironmentPreparingListenerTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/LogEnvironmentPreparingListenerTests.java index 4324e0bd6..891b39334 100644 --- a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/LogEnvironmentPreparingListenerTests.java +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/LogEnvironmentPreparingListenerTests.java @@ -26,9 +26,6 @@ /** * Tests for {@link LogEnvironmentPostProcessor}. - * - * @author huzijie - * @version LogEnvironmentPreparingListenerTests.java, v 0.1 2023年02月01日 3:35 PM huzijie Exp $ */ public class LogEnvironmentPreparingListenerTests { diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoderTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoderTests.java new file mode 100644 index 000000000..4e6c822ca --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/logging/logback/SofaBootStructuredLogEncoderTests.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alipay.sofa.boot.logging.logback; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +public class SofaBootStructuredLogEncoderTests { + + private static final String FORMAT_PROPERTY = "FILE_LOG_STRUCTURED_FORMAT"; + + @AfterEach + void clearStructuredFormatProperty() { + System.clearProperty(FORMAT_PROPERTY); + } + + @Test + void usesPatternOutputWhenStructuredLoggingIsDisabled() { + LoggerContext context = new LoggerContext(); + SofaBootStructuredLogEncoder encoder = createEncoder(context); + + encoder.start(); + + assertThat(encoder.headerBytes()).isEmpty(); + assertThat(encoder.footerBytes()).isEmpty(); + assertThat(new String(encoder.encode(createEvent(context)), StandardCharsets.UTF_8)) + .isEqualTo("hello structured logging\n"); + + encoder.stop(); + context.stop(); + } + + @Test + void usesSpringBootStructuredEncoderWhenFormatPropertyIsSet() { + System.setProperty(FORMAT_PROPERTY, "logstash"); + LoggerContext context = new LoggerContext(); + context.putObject(Environment.class.getName(), + new MockEnvironment().withProperty("spring.application.name", "test-app")); + SofaBootStructuredLogEncoder encoder = createEncoder(context); + + encoder.start(); + + String output = new String(encoder.encode(createEvent(context)), StandardCharsets.UTF_8); + assertThat(output).startsWith("{").contains("\"message\":\"hello structured logging\"") + .contains("\"logger_name\":\"test.logger\"").contains("\"level\":\"INFO\""); + + encoder.stop(); + context.stop(); + } + + @Test + void fallsBackToPatternOutputWhenStructuredEncoderCannotBeInitialized() { + System.setProperty(FORMAT_PROPERTY, "logstash"); + LoggerContext context = new LoggerContext(); + SofaBootStructuredLogEncoder encoder = createEncoder(context); + + encoder.start(); + + assertThat(new String(encoder.encode(createEvent(context)), StandardCharsets.UTF_8)) + .isEqualTo("hello structured logging\n"); + + encoder.stop(); + context.stop(); + } + + @Test + void requiresPatternBeforeStart() { + SofaBootStructuredLogEncoder encoder = new SofaBootStructuredLogEncoder(); + encoder.setContext(new LoggerContext()); + encoder.setFormatProperty(FORMAT_PROPERTY); + + assertThatIllegalStateException().isThrownBy(encoder::start) + .withMessage("Pattern has not been set"); + } + + private SofaBootStructuredLogEncoder createEncoder(LoggerContext context) { + SofaBootStructuredLogEncoder encoder = new SofaBootStructuredLogEncoder(); + encoder.setContext(context); + encoder.setPattern("%msg%n"); + encoder.setFormatProperty(FORMAT_PROPERTY); + encoder.setCharset(StandardCharsets.UTF_8); + return encoder; + } + + private LoggingEvent createEvent() { + return createEvent(new LoggerContext()); + } + + private LoggingEvent createEvent(LoggerContext context) { + LoggingEvent event = new LoggingEvent(); + event.setLoggerContext(context); + event.setLoggerName("test.logger"); + event.setLevel(Level.INFO); + event.setMessage("hello structured logging"); + event.setThreadName("main"); + event.setTimeStamp(Instant.parse("2024-08-23T10:15:30Z").toEpochMilli()); + event.setMDCPropertyMap(Collections.emptyMap()); + return event; + } +}