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 79640714a1e5..88cd5eacbcab 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 @@ -43,6 +43,11 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter resourceAttributes() { Map 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 +92,10 @@ private String getApplicationName() { return this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); } + private String getApplicationGroup() { + return this.environment.getProperty("spring.application.group", DEFAULT_APPLICATION_GROUP); + } + @Override public Map 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 622ad4371b07..3ccdf9eba878 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 @@ -53,8 +53,15 @@ public class OpenTelemetryAutoConfiguration { */ private static final String DEFAULT_APPLICATION_NAME = "unknown_service"; + /** + * Default value for application group if {@code spring.application.group} is not set. + */ + private static final String DEFAULT_APPLICATION_GROUP = "unknown_group"; + 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,8 +79,10 @@ OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @ConditionalOnMissingBean Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + String applicationGroup = environment.getProperty("spring.application.group", DEFAULT_APPLICATION_GROUP); return Resource.getDefault() .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName))) + .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_GROUP, applicationGroup))) .merge(toResource(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 dd6c3f199204..38d67a5f824c 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 @@ void shouldUseDefaultApplicationNameIfApplicationNameIsNotSet() { 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()).containsEntry("service.group", "unknown_group"); + } + private OtlpPropertiesConfigAdapter createAdapter() { return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.connectionDetails, this.environment); diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/properties-and-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/properties-and-configuration.adoc index 40184bd008d4..0e091e073fed 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/properties-and-configuration.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/properties-and-configuration.adoc @@ -204,6 +204,7 @@ The preceding example YAML corresponds to the following `application.properties` [source,properties,subs="verbatim",configprops] ---- spring.application.name=cruncher +spring.application.group=crunchGroup spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost/test server.port=9000 diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index 8761abffa592..c56f1ad57a84 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -88,9 +88,10 @@ logging: pattern: correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] " include-application-name: false + include-application-group: false ---- -NOTE: In the example above, configprop:logging.include-application-name[] is set to `false` to avoid the application name being duplicated in the log messages (configprop:logging.pattern.correlation[] already contains it). +NOTE: In the example above, configprop:logging.include-application-name[] and configprop:logging.include-application-group[] is set to `false` to avoid the application name being duplicated in the log messages (configprop:logging.pattern.correlation[] already contains it). It's also worth mentioning that configprop:logging.pattern.correlation[] contains a trailing space so that it is separated from the logger name that comes right after it by default. 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 746f25c6feec..38cd6a0219c1 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). @@ -43,6 +44,7 @@ NOTE: Logback does not have a `FATAL` level. 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`. @@ -544,12 +546,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 a39c8afbdb0a..59c7c9e0cc7e 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 @@ private PropertyResolver getPropertyResolver() { 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 @@ private void setApplicationNameSystemProperty(PropertyResolver resolver) { } } + 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.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 489ebec89feb..6ef25a84e4fa 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. + */ + 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 000000000000..8042a62df0ee --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ApplicationGroupConverter.java @@ -0,0 +1,50 @@ +/* + * 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#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.APPLICATION_GROUP.getEnvironmentVariableName()); + if (applicationGroup == null) { + applicationGroup = System.getProperty(LoggingSystemProperty.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 abba6c24c41d..26d5375c0232 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 @@ -69,6 +69,7 @@ void apply(LogbackConfigurator config) { private void defaults(LogbackConfigurator config) { config.conversionRule("applicationName", ApplicationNameConverter.class); + config.conversionRule("applicationGroup", ApplicationGroupConverter.class); config.conversionRule("clr", ColorConverter.class); config.conversionRule("correlationId", CorrelationIdConverter.class); config.conversionRule("wex", WhitespaceThrowableProxyConverter.class); @@ -76,7 +77,7 @@ private void defaults(LogbackConfigurator config) { 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[%15.15t]){faint} %clr(---){faint} %clr(%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 @@ private void defaults(LogbackConfigurator config) { 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[%t] --- %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 f73eac890b6e..8285ef57b5f3 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 @@ private void registerHintsForBuiltInLogbackConverters(ReflectionHints reflection } 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/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 916f1126db61..a078a2bfd7e3 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:-}[%15.15t]}{faint} %clr{---}{faint} %clr{${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 239f2e35a34c..5bd996b6bc80 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:-}[%15.15t]} {faint}%clr{---}{faint} %clr{${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 a469b4cba9c4..a641a362166a 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 @@ -6,15 +6,16 @@ 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 b070493fe62d..3f81feaeecaf 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 @@ void loggedApplicationNameWhenApplicationNameLoggingDisabled() { assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_NAME)).isNull(); } + @Test + void loggedApplicationGroupWhenHasApplicationGroup() { + new LoggingSystemProperties(new MockEnvironment().withProperty("spring.application.group", "test")).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_GROUP)).isEqualTo("[test] "); + } + + @Test + void loggedApplicationGroupWhenHasNoApplicationGroup() { + new LoggingSystemProperties(new MockEnvironment()).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.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.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 7d089c175089..4fcc4ad6ead2 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 @@ void applicationNameLoggingToFileWhenDisabled() { .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 000000000000..5375814301fc --- /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.APPLICATION_GROUP.getEnvironmentVariableName()); + } + else { + System.setProperty(LoggingSystemProperty.APPLICATION_GROUP.getEnvironmentVariableName(), group); + } + try { + action.run(); + } + finally { + System.clearProperty(LoggingSystemProperty.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 995c25f8804b..fbd182b3524d 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 @@ void applyingSystemPropertiesDoesNotCauseUnwantedStatusWarnings(CapturedOutput o 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/log4j2.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml index 1c84d286b093..bcda4f2453c8 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:-}[%15.15t]}{faint} %clr{---}{faint} %clr{${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 2c35d22ff033..381f50751e61 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,5 @@ spring.application.name=sample +spring.application.group=sample-group service.name=Phil spring.security.user.name=user