diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index 79640714a1..d30e8b7eb0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties. import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.core.env.Environment; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * Adapter to convert {@link OtlpProperties} to an {@link OtlpConfig}. @@ -79,6 +80,7 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter result = new HashMap<>((!CollectionUtils.isEmpty(resourceAttributes)) ? resourceAttributes : get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes)); result.computeIfAbsent("service.name", (key) -> getApplicationName()); + result.computeIfAbsent("service.group", (key) -> getApplicationGroup()); return Collections.unmodifiableMap(result); } @@ -86,6 +88,11 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter headers() { return get(OtlpProperties::getHeaders, OtlpConfig.super::headers); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 622ad4371b..720e79142f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. @@ -55,6 +56,8 @@ public class OpenTelemetryAutoConfiguration { private static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); + private static final AttributeKey ATTRIBUTE_KEY_SERVICE_GROUP = AttributeKey.stringKey("service.group"); + @Bean @ConditionalOnMissingBean(OpenTelemetry.class) OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @@ -72,9 +75,13 @@ public class OpenTelemetryAutoConfiguration { @ConditionalOnMissingBean Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); - return Resource.getDefault() - .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName))) - .merge(toResource(properties)); + String applicationGroup = environment.getProperty("spring.application.group"); + Resource resource = Resource.getDefault() + .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName))); + if (StringUtils.hasLength(applicationGroup)) { + resource = resource.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_GROUP, applicationGroup))); + } + return resource.merge(toResource(properties)); } private static Resource toResource(OpenTelemetryProperties properties) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index dd6c3f1992..5413e8c3a2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -139,6 +139,32 @@ class OtlpPropertiesConfigAdapterTests { assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "unknown_service"); } + @Test + @SuppressWarnings("removal") + void serviceGroupOverridesApplicationGroup() { + this.environment.setProperty("spring.application.group", "alpha"); + this.properties.setResourceAttributes(Map.of("service.group", "beta")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.group", "beta"); + } + + @Test + void serviceGroupOverridesApplicationGroupWhenUsingOtelProperties() { + this.environment.setProperty("spring.application.group", "alpha"); + this.openTelemetryProperties.setResourceAttributes(Map.of("service.group", "beta")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.group", "beta"); + } + + @Test + void shouldUseApplicationGroupIfServiceGroupIsNotSet() { + this.environment.setProperty("spring.application.group", "alpha"); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.group", "alpha"); + } + + @Test + void shouldUseDefaultApplicationGroupIfApplicationGroupIsNotSet() { + assertThat(createAdapter().resourceAttributes()).doesNotContainKey("service.group"); + } + private OtlpPropertiesConfigAdapter createAdapter() { return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.connectionDetails, this.environment); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java index 4b84037bbe..6848c8003d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -90,6 +90,23 @@ class OpenTelemetryAutoConfigurationTests { }); } + @Test + void shouldApplySpringApplicationGroupToResource() { + this.runner.withPropertyValues("spring.application.group=my-group").run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()) + .contains(entry(AttributeKey.stringKey("service.group"), "my-group")); + }); + } + + @Test + void shouldNotApplySpringApplicationGroupIfNotSet() { + this.runner.run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()).doesNotContainKey(AttributeKey.stringKey("service.group")); + }); + } + @Test void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { this.runner.run((context) -> { diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 949ee36353..7856a7a159 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -34,6 +34,7 @@ The following items are output: * Process ID. * A `---` separator to distinguish the start of actual log messages. * Application name: Enclosed in square brackets (logged by default only if configprop:spring.application.name[] is set) +* Application group: Enclosed in square brackets (logged by default only if configprop:spring.application.group[] is set) * Thread name: Enclosed in square brackets (may be truncated for console output). * Correlation ID: If tracing is enabled (not shown in the sample above) * Logger name: This is usually the source class name (often abbreviated). @@ -44,6 +45,8 @@ It is mapped to `ERROR`. TIP: If you have a configprop:spring.application.name[] property but don't want it logged you can set configprop:logging.include-application-name[] to `false`. +TIP: If you have a configprop:spring.application.group[] property but don't want it logged you can set configprop:logging.include-application-group[] to `false`. + [[features.logging.console-output]] @@ -544,12 +547,13 @@ The following listing shows three sample profiles: If you want to refer to properties from your Spring `Environment` within your Log4j2 configuration you can use `spring:` prefixed https://logging.apache.org/log4j/2.x/manual/lookups.html[lookups]. Doing so can be useful if you want to access values from your `application.properties` file in your Log4j2 configuration. -The following example shows how to set a Log4j2 property named `applicationName` that reads `spring.application.name` from the Spring `Environment`: +The following example shows how to set a Log4j2 property named `applicationName` and `applicationGroup` that reads `spring.application.name` and `spring.application.group` from the Spring `Environment`: [source,xml] ---- ${spring:spring.application.name} + ${spring:spring.application.property} ---- diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java index 51a03bb308..71c2f0dd61 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java @@ -229,6 +229,7 @@ public class LoggingSystemProperties { protected void apply(LogFile logFile, PropertyResolver resolver) { String defaultCharsetName = getDefaultCharset().name(); setApplicationNameSystemProperty(resolver); + setApplicationGroupSystemProperty(resolver); setSystemProperty(LoggingSystemProperty.PID, new ApplicationPid().toString()); setSystemProperty(LoggingSystemProperty.CONSOLE_CHARSET, resolver, defaultCharsetName); setSystemProperty(LoggingSystemProperty.FILE_CHARSET, resolver, defaultCharsetName); @@ -255,6 +256,16 @@ public class LoggingSystemProperties { } } + private void setApplicationGroupSystemProperty(PropertyResolver resolver) { + if (resolver.getProperty("logging.include-application-group", Boolean.class, Boolean.TRUE)) { + String applicationGroup = resolver.getProperty("spring.application.group"); + if (StringUtils.hasText(applicationGroup)) { + setSystemProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName(), + "[%s] ".formatted(applicationGroup)); + } + } + } + private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver) { setSystemProperty(property, resolver, Function.identity()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java index 489ebec89f..cf8e5dde47 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java @@ -30,6 +30,11 @@ public enum LoggingSystemProperty { */ APPLICATION_NAME("LOGGED_APPLICATION_NAME"), + /** + * Logging system property for the application group that should be logged. + */ + LOGGED_APPLICATION_GROUP("LOGGED_APPLICATION_GROUP"), + /** * Logging system property for the process ID. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ApplicationGroupConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ApplicationGroupConverter.java new file mode 100644 index 0000000000..21d8caca4f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ApplicationGroupConverter.java @@ -0,0 +1,51 @@ +/* + * 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.ClassicConverter; +import ch.qos.logback.classic.pattern.PropertyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import org.springframework.boot.logging.LoggingSystemProperty; + +/** + * Logback {@link ClassicConverter} to convert the + * {@link LoggingSystemProperty#LOGGED_APPLICATION_GROUP APPLICATION_GROUP} into a value + * suitable for logging. Similar to Logback's {@link PropertyConverter} but a non-existent + * property is logged as an empty string rather than {@code null}. + * + * @author Jakob Wanger + * @since 3.4.0 + */ +public class ApplicationGroupConverter extends ClassicConverter { + + @Override + public String convert(ILoggingEvent event) { + String applicationGroup = event.getLoggerContextVO() + .getPropertyMap() + .get(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName()); + if (applicationGroup == null) { + applicationGroup = System + .getProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName()); + if (applicationGroup == null) { + applicationGroup = ""; + } + } + return applicationGroup; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java index abba6c24c4..0feee7bbfc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java @@ -68,6 +68,7 @@ class DefaultLogbackConfiguration { } private void defaults(LogbackConfigurator config) { + config.conversionRule("applicationGroup", ApplicationGroupConverter.class); config.conversionRule("applicationName", ApplicationNameConverter.class); config.conversionRule("clr", ColorConverter.class); config.conversionRule("correlationId", CorrelationIdConverter.class); @@ -76,7 +77,7 @@ class DefaultLogbackConfiguration { config.getContext() .putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-" + "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) " - + "%clr(${PID:- }){magenta} %clr(---){faint} %clr(%applicationName[%15.15t]){faint} " + + "%clr(${PID:- }){magenta} %clr(---){faint} %clr(%applicationName%applicationGroup[%15.15t]){faint} " + "%clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} " + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); String defaultCharset = Charset.defaultCharset().name(); @@ -85,7 +86,7 @@ class DefaultLogbackConfiguration { config.getContext().putProperty("CONSOLE_LOG_THRESHOLD", resolve(config, "${CONSOLE_LOG_THRESHOLD:-TRACE}")); config.getContext() .putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-" - + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- %applicationName[%t] " + + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- %applicationName%applicationGroup[%t] " + "${LOG_CORRELATION_PATTERN:-}" + "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); config.getContext() diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java index f73eac890b..8285ef57b5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java @@ -58,9 +58,9 @@ class LogbackRuntimeHints implements RuntimeHintsRegistrar { } private void registerHintsForSpringBootConverters(ReflectionHints reflection) { - registerForPublicConstructorInvocation(reflection, ApplicationNameConverter.class, ColorConverter.class, - ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class, - CorrelationIdConverter.class); + registerForPublicConstructorInvocation(reflection, ApplicationNameConverter.class, + ApplicationGroupConverter.class, ColorConverter.class, ExtendedWhitespaceThrowableProxyConverter.class, + WhitespaceThrowableProxyConverter.class, CorrelationIdConverter.class); } private void registerForPublicConstructorInvocation(ReflectionHints reflection, Class... classes) { diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 3484dd4ad1..7449773a9a 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -103,6 +103,13 @@ "description": "Log groups to quickly change multiple loggers at the same time. For instance, `logging.group.db=org.hibernate,org.springframework.jdbc`.", "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener" }, + { + "name": "logging.include-application-group", + "type": "java.lang.Boolean", + "description": "Whether to include the application group in the logs.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": true + }, { "name": "logging.include-application-name", "type": "java.lang.Boolean", @@ -228,6 +235,11 @@ "description": "Log level threshold for file output.", "defaultValue": "TRACE" }, + { + "name": "spring.application.group", + "type": "java.lang.String", + "description": "Application group." + }, { "name": "spring.application.index", "type": "java.lang.Integer", diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index 916f1126db..094c991b2c 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}${sys:LOGGED_APPLICATION_GROUP:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}${sys:LOGGED_APPLICATION_GROUP:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml index 239f2e35a3..0e36758fbc 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}${sys:LOGGED_APPLICATION_GROUP:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}${sys:LOGGED_APPLICATION_GROUP:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml index a469b4cba9..c4f4681111 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml @@ -5,16 +5,17 @@ Default logback configuration provided for import --> + - + - + diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java index b070493fe6..c2d9481189 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java @@ -156,6 +156,25 @@ class LoggingSystemPropertiesTests { assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_NAME)).isNull(); } + @Test + void loggedApplicationGroupWhenHasApplicationGroup() { + new LoggingSystemProperties(new MockEnvironment().withProperty("spring.application.group", "test")).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP)).isEqualTo("[test] "); + } + + @Test + void loggedApplicationGroupWhenHasNoApplicationGroup() { + new LoggingSystemProperties(new MockEnvironment()).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP)).isNull(); + } + + @Test + void loggedApplicationGroupWhenApplicationGroupLoggingDisabled() { + new LoggingSystemProperties(new MockEnvironment().withProperty("spring.application.group", "test") + .withProperty("logging.include-application-group", "false")).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP)).isNull(); + } + @Test void shouldSupportFalseConsoleThreshold() { new LoggingSystemProperties(new MockEnvironment().withProperty("logging.threshold.console", "false")) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 7d089c1750..4fcc4ad6ea 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -651,6 +651,79 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { .doesNotContain("myapp"); } + @Test + void applicationGroupLoggingToConsoleWhenHasApplicationGroup(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "mygroup"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[mygroup] "); + } + + @Test + void applicationGroupLoggingToConsoleWhenHasApplicationGroupWithParenthesis(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "mygroup (dev)"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[mygroup (dev)] "); + } + + @Test + void applicationGroupLoggingToConsoleWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "application-group"); + this.environment.setProperty("logging.include-application-group", "false"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("${sys:LOGGED_APPLICATION_GROUP}") + .doesNotContain("myapp"); + } + + @Test + void applicationGroupLoggingToFileWhenHasApplicationGroup() { + this.environment.setProperty("spring.application.group", "mygroup"); + new LoggingSystemProperties(this.environment).apply(); + File file = new File(tmpDir(), "log4j2-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).contains("[mygroup] "); + } + + @Test + void applicationGroupLoggingToFileWhenHasApplicationGroupWithParenthesis() { + this.environment.setProperty("spring.application.group", "mygroup (dev)"); + new LoggingSystemProperties(this.environment).apply(); + File file = new File(tmpDir(), "log4j2-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).contains("[mygroup (dev)] "); + } + + @Test + void applicationGroupLoggingToFileWhenDisabled() { + this.environment.setProperty("spring.application.group", "application-group"); + this.environment.setProperty("logging.include-application-group", "false"); + new LoggingSystemProperties(this.environment).apply(); + File file = new File(tmpDir(), "log4j2-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).doesNotContain("${sys:LOGGED_APPLICATION_GROUP}") + .doesNotContain("myapp"); + } + @Test void shouldNotContainAnsiEscapeCodes(CapturedOutput output) { this.loggingSystem.beforeInitialize(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ApplicationGroupConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ApplicationGroupConverterTests.java new file mode 100644 index 0000000000..cc3bad8a52 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ApplicationGroupConverterTests.java @@ -0,0 +1,81 @@ +/* + * 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.Collections; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggerContextVO; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.logging.LoggingSystemProperty; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationGroupConverter}. + * + * @author Jakob Wanger + */ +class ApplicationGroupConverterTests { + + private final ApplicationGroupConverter converter; + + private final LoggingEvent event = new LoggingEvent(); + + ApplicationGroupConverterTests() { + this.converter = new ApplicationGroupConverter(); + this.converter.setContext(new LoggerContext()); + this.event.setLoggerContextRemoteView( + new LoggerContextVO("test", Collections.emptyMap(), System.currentTimeMillis())); + } + + @Test + void whenNoLoggedApplicationGroupConvertReturnsEmptyString() { + withLoggedApplicationGroup(null, () -> { + this.converter.start(); + String converted = this.converter.convert(this.event); + assertThat(converted).isEqualTo(""); + }); + } + + @Test + void whenLoggedApplicationGroupConvertReturnsIt() { + withLoggedApplicationGroup("my-application", () -> { + this.converter.start(); + String converted = this.converter.convert(this.event); + assertThat(converted).isEqualTo("my-application"); + }); + } + + private void withLoggedApplicationGroup(String group, Runnable action) { + if (group == null) { + System.clearProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName()); + } + else { + System.setProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName(), group); + } + try { + action.run(); + } + finally { + System.clearProperty(LoggingSystemProperty.LOGGED_APPLICATION_GROUP.getEnvironmentVariableName()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 995c25f880..fbd182b352 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -876,6 +876,62 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { assertThat(output).doesNotContain("WARN"); } + @Test + void applicationGroupLoggingToConsoleWhenHasApplicationGroup(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "mygroup"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[mygroup] "); + } + + @Test + void applicationGroupLoggingToConsoleWhenHasApplicationGroupWithParenthesis(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "mygroup (dev)"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[mygroup (dev)] "); + } + + @Test + void applicationGroupLoggingToConsoleWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "mygroup"); + this.environment.setProperty("logging.include-application-group", "false"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("mygroup").doesNotContain("null"); + } + + @Test + void applicationGroupLoggingToFileWhenHasApplicationGroup() { + this.environment.setProperty("spring.application.group", "mygroup"); + File file = new File(tmpDir(), "logback-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).contains("[mygroup] "); + } + + @Test + void applicationGroupLoggingToFileWhenHasApplicationGroupWithParenthesis() { + this.environment.setProperty("spring.application.group", "mygroup (dev)"); + File file = new File(tmpDir(), "logback-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).contains("[mygroup (dev)] "); + } + + @Test + void applicationGroupLoggingToFileWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.group", "myGroup"); + this.environment.setProperty("logging.include-application-group", "false"); + File file = new File(tmpDir(), "logback-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + initialize(this.initializationContext, null, logFile); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")).doesNotContain("myGroup").doesNotContain("null"); + } + @Test void shouldNotContainAnsiEscapeCodes(CapturedOutput output) { this.loggingSystem.beforeInitialize(); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties index d622bb1f7c..1039e00ddc 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties @@ -1,4 +1,7 @@ spring.application.name=sample +spring.application.group=sample-group +#logging.include-application-name=false +#logging.include-application-group=false spring.security.user.name=user spring.security.user.password=password management.endpoint.shutdown.enabled=true diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml index 1c84d286b0..ba8b3efd9c 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ ???? - %clr{%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%xwEx + %clr{%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}${sys:LOGGED_APPLICATION_GROUP:-}[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%xwEx diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 2c35d22ff0..e0c09b2a3b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -1,4 +1,7 @@ spring.application.name=sample +spring.application.group=sample-group +#logging.include-application-name=false +#logging.include-application-group=false service.name=Phil spring.security.user.name=user