docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
| Component | Purpose | Thread Safety | Key Features |
|---|---|---|---|
LoggingSystem | Abstract logging framework | Thread-safe | Framework abstraction, level configuration |
LoggingSystemFactory | Creates logging system instances | Thread-safe | Auto-detection, custom implementations |
LogbackLoggingSystem | Logback implementation | Thread-safe | Most common, rich features |
Log4J2LoggingSystem | Log4j2 implementation | Thread-safe | High performance, async logging |
JavaLoggingSystem | JUL implementation | Thread-safe | No dependencies, basic features |
LogLevel | Log level enumeration | Thread-safe (enum) | TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF |
LoggerConfiguration | Logger configuration holder | Thread-safe (immutable) | Level, effective level, name |
LoggerGroups | Grouped logger management | Thread-safe | Bulk level changes, predefined groups |
| Level | Priority | Usage | Thread Safety |
|---|---|---|---|
TRACE | Lowest | Fine-grained debugging | Thread-safe |
DEBUG | Low | Detailed debugging information | Thread-safe |
INFO | Normal | General informational messages | Thread-safe |
WARN | Elevated | Warning messages | Thread-safe |
ERROR | High | Error messages | Thread-safe |
FATAL | Critical (Log4j2 only) | Fatal error messages | Thread-safe |
OFF | Highest | Disable logging | Thread-safe |
| Feature | Purpose | Thread Safety | Supported Frameworks |
|---|---|---|---|
| Correlation ID | Request tracking | Thread-safe (MDC) | Logback, Log4j2 |
| JSON formatting | Machine-readable logs | Thread-safe | Logback, Log4j2 |
| ECS formatting | Elastic Common Schema | Thread-safe | Logback, Log4j2 |
| Key-value pairs | Structured data | Thread-safe | Logback 1.3+, Log4j2 |
| MDC (Mapped Diagnostic Context) | Thread-local context | Thread-safe | All frameworks |
| Property | Purpose | Default | Example |
|---|---|---|---|
logging.level.* | Set log levels | INFO | logging.level.root=WARN |
logging.pattern.console | Console output pattern | Framework default | %d{yyyy-MM-dd HH:mm:ss} - %msg%n |
logging.pattern.file | File output pattern | Framework default | %d %p %c{1} [%t] %m%n |
logging.file.name | Log file path | None | /var/log/app.log |
logging.file.path | Log file directory | None | /var/log |
logging.logback.rollingpolicy.max-file-size | Max file size | 10MB | 20MB |
logging.logback.rollingpolicy.max-history | Days to retain | 7 | 30 |
logging.group.* | Define logger groups | None | logging.group.db=org.hibernate,org.jooq |
The Spring Boot logging system provides a comprehensive abstraction over popular Java logging frameworks including Logback, Log4j2, and Java Util Logging. It offers powerful features for configuring log levels, output formats, structured logging, and deferred logging.
The LoggingSystem abstract class is the central abstraction for all logging implementations in Spring Boot.
package org.springframework.boot.logging;
/**
* Common abstraction over logging systems.
*
* Subclasses should provide implementations for specific logging frameworks
* like Logback, Log4j2, or Java Util Logging.
*
* @since 1.0.0
*/
public abstract class LoggingSystem {
/**
* System property that can be used to indicate the LoggingSystem to use.
* Value is "org.springframework.boot.logging.LoggingSystem" (derived from LoggingSystem.class.getName()).
*/
public static final String SYSTEM_PROPERTY = "org.springframework.boot.logging.LoggingSystem";
/**
* Value that can be used to indicate that no LoggingSystem should be used.
*/
public static final String NONE = "none";
/**
* Name used for the root logger.
*/
public static final String ROOT_LOGGER_NAME = "ROOT";
/**
* Environment property used to indicate that a correlation ID is expected.
*
* @since 3.2.0
*/
public static final String EXPECT_CORRELATION_ID_PROPERTY = "logging.expect-correlation-id";
/**
* Returns the LoggingSystemProperties that should be applied.
*
* @param environment the ConfigurableEnvironment used to obtain values
* @return the LoggingSystemProperties to apply
* @since 2.4.0
*/
public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) {
return new LoggingSystemProperties(environment);
}
/**
* Reset the logging system to limit output. Called before initialize()
* to reduce logging noise until the system is fully initialized.
*/
public abstract void beforeInitialize();
/**
* Fully initialize the logging system.
*
* @param initializationContext the logging initialization context
* @param configLocation a log configuration location or {@code null} for default
* @param logFile the log output file or {@code null} for console only
*/
public void initialize(LoggingInitializationContext initializationContext,
@Nullable String configLocation,
@Nullable LogFile logFile) {
}
/**
* Clean up the logging system. Subclasses should override to perform
* any logging system-specific cleanup.
*/
public void cleanUp() {
}
/**
* Returns a Runnable that can handle shutdown of this logging system
* when the JVM exits.
*
* @return the shutdown handler, or {@code null}
*/
@Nullable
public Runnable getShutdownHandler() {
return null;
}
/**
* Returns the set of LogLevels actually supported by the logging system.
*
* @return the supported levels
*/
public Set<LogLevel> getSupportedLogLevels() {
return EnumSet.allOf(LogLevel.class);
}
/**
* Sets the logging level for a given logger.
*
* @param loggerName the name of the logger (null can be used for root logger)
* @param level the log level (null to remove custom level and use default)
*/
public void setLogLevel(@Nullable String loggerName, @Nullable LogLevel level) {
throw new UnsupportedOperationException("Unable to set log level");
}
/**
* Returns the current configuration for all loggers.
*
* @return the current configurations
* @since 1.5.0
*/
public List<LoggerConfiguration> getLoggerConfigurations() {
throw new UnsupportedOperationException("Unable to get logger configurations");
}
/**
* Returns the current configuration for a specific logger.
*
* @param loggerName the name of the logger
* @return the current configuration, or null if the logger does not exist
* @since 1.5.0
*/
public @Nullable LoggerConfiguration getLoggerConfiguration(String loggerName) {
throw new UnsupportedOperationException("Unable to get logger configuration");
}
/**
* Detect and return the logging system in use. Supports Logback, Log4j2,
* and Java Util Logging.
*
* @param classLoader the classloader
* @return the logging system
*/
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystemClassName)) {
if (NONE.equals(loggingSystemClassName)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystemClassName);
}
LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
Assert.state(loggingSystem != null, "No suitable logging system located");
return loggingSystem;
}
}Factory interface for creating LoggingSystem instances. Spring Boot uses the service loader pattern to discover available logging systems.
package org.springframework.boot.logging;
/**
* Factory class used by LoggingSystem.get(ClassLoader) to find an actual
* implementation.
*
* @since 2.4.0
*/
public interface LoggingSystemFactory {
/**
* Return a logging system implementation or null if no logging system
* is available.
*
* @param classLoader the class loader to use
* @return a logging system
*/
LoggingSystem getLoggingSystem(ClassLoader classLoader);
/**
* Return a LoggingSystemFactory backed by spring.factories.
*
* @return a LoggingSystemFactory instance
*/
static LoggingSystemFactory fromSpringFactories() {
return new DelegatingLoggingSystemFactory(
(classLoader) -> SpringFactoriesLoader.loadFactories(
LoggingSystemFactory.class, classLoader));
}
}Usage Example:
// Get the logging system for the current classloader
LoggingSystem loggingSystem = LoggingSystem.get(getClass().getClassLoader());
// Set log level programmatically
loggingSystem.setLogLevel("com.example.myapp", LogLevel.DEBUG);
// Get all logger configurations
List<LoggerConfiguration> configs = loggingSystem.getLoggerConfigurations();
for (LoggerConfiguration config : configs) {
System.out.println(config.getName() + ": " + config.getEffectiveLevel());
}Enumeration of log levels supported by the logging system.
package org.springframework.boot.logging;
/**
* Logging levels supported by a LoggingSystem.
*
* @since 1.0.0
*/
public enum LogLevel {
TRACE(Log::trace),
DEBUG(Log::debug),
INFO(Log::info),
WARN(Log::warn),
ERROR(Log::error),
FATAL(Log::fatal),
OFF(null);
/**
* Log a message to the given logger at this level.
*
* @param logger the logger
* @param message the message to log
* @since 3.1.0
*/
public void log(Log logger, Object message) {
log(logger, message, null);
}
/**
* Log a message to the given logger at this level.
*
* @param logger the logger
* @param message the message to log
* @param cause the cause to log
* @since 3.1.0
*/
public void log(Log logger, Object message, Throwable cause) {
if (logger != null && this.logMethod != null) {
this.logMethod.log(logger, message, cause);
}
}
}Represents a reference to a log output file.
package org.springframework.boot.logging;
/**
* A reference to a log output file. Log output files are specified using
* logging.file.name or logging.file.path properties.
*
* @since 1.2.1
*/
public class LogFile {
/**
* Property that contains the name of the log file.
*
* @since 2.2.0
*/
public static final String FILE_NAME_PROPERTY = "logging.file.name";
/**
* Property that contains the directory where log files are written.
*
* @since 2.2.0
*/
public static final String FILE_PATH_PROPERTY = "logging.file.path";
/**
* Apply log file details to LOG_PATH and LOG_FILE system properties.
*/
public void applyToSystemProperties() {
applyTo(System.getProperties());
}
/**
* Apply log file details to LOG_PATH and LOG_FILE map entries.
*
* @param properties the properties to apply to
*/
public void applyTo(Properties properties) {
put(properties, LoggingSystemProperty.LOG_PATH, this.path);
put(properties, LoggingSystemProperty.LOG_FILE, toString());
}
/**
* Get a LogFile from the given Spring Environment.
*
* @param propertyResolver the PropertyResolver used to obtain logging properties
* @return a LogFile or null if environment didn't contain suitable properties
*/
public static LogFile get(PropertyResolver propertyResolver) {
String file = propertyResolver.getProperty(FILE_NAME_PROPERTY);
String path = propertyResolver.getProperty(FILE_PATH_PROPERTY);
if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
return new LogFile(file, path);
}
return null;
}
}Immutable class representing the configuration of a logger.
package org.springframework.boot.logging;
/**
* Immutable class that represents the configuration of a LoggingSystem's logger.
*
* @since 1.5.0
*/
public final class LoggerConfiguration {
/**
* Create a new LoggerConfiguration instance.
*
* @param name the name of the logger
* @param configuredLevel the configured level of the logger
* @param effectiveLevel the effective level of the logger
*/
public LoggerConfiguration(String name, LogLevel configuredLevel, LogLevel effectiveLevel) {
Assert.notNull(name, "'name' must not be null");
Assert.notNull(effectiveLevel, "'effectiveLevel' must not be null");
this.name = name;
this.levelConfiguration = (configuredLevel != null)
? LevelConfiguration.of(configuredLevel) : null;
this.inheritedLevelConfiguration = LevelConfiguration.of(effectiveLevel);
}
/**
* Create a new LoggerConfiguration instance.
*
* @param name the name of the logger
* @param levelConfiguration the level configuration
* @param inheritedLevelConfiguration the inherited level configuration
* @since 2.7.13
*/
public LoggerConfiguration(String name, LevelConfiguration levelConfiguration,
LevelConfiguration inheritedLevelConfiguration) {
Assert.notNull(name, "'name' must not be null");
Assert.notNull(inheritedLevelConfiguration, "'inheritedLevelConfiguration' must not be null");
this.name = name;
this.levelConfiguration = levelConfiguration;
this.inheritedLevelConfiguration = inheritedLevelConfiguration;
}
/**
* Returns the name of the logger.
*
* @return the name of the logger
*/
public String getName() {
return this.name;
}
/**
* Returns the configured level of the logger.
*
* @return the configured level of the logger
*/
public LogLevel getConfiguredLevel() {
LevelConfiguration configuration = getLevelConfiguration(ConfigurationScope.DIRECT);
return (configuration != null) ? configuration.getLevel() : null;
}
/**
* Returns the effective level of the logger.
*
* @return the effective level of the logger
*/
public LogLevel getEffectiveLevel() {
return getLevelConfiguration().getLevel();
}
/**
* Return the level configuration, considering inherited loggers.
*
* @return the level configuration
* @since 2.7.13
*/
public LevelConfiguration getLevelConfiguration() {
LevelConfiguration result = getLevelConfiguration(ConfigurationScope.INHERITED);
Assert.state(result != null, "Inherited level configuration must not be null");
return result;
}
/**
* Return the level configuration for the given scope.
*
* @param scope the configuration scope
* @return the level configuration or null for DIRECT scope without applied configuration
* @since 2.7.13
*/
public LevelConfiguration getLevelConfiguration(ConfigurationScope scope) {
return (scope != ConfigurationScope.DIRECT)
? this.inheritedLevelConfiguration
: this.levelConfiguration;
}
/**
* Supported logger configuration scopes.
*
* @since 2.7.13
*/
public enum ConfigurationScope {
/**
* Only return configuration applied directly (configured/assigned).
*/
DIRECT,
/**
* May return configuration applied to a parent logger (effective).
*/
INHERITED
}
/**
* Logger level configuration.
*
* @since 2.7.13
*/
public static final class LevelConfiguration {
/**
* Return the name of the level.
*
* @return the level name
*/
public String getName() {
return this.name;
}
/**
* Return the actual level value if possible.
*
* @return the level value
* @throws IllegalStateException if this is a custom level
*/
public LogLevel getLevel() {
Assert.state(this.logLevel != null,
() -> "Unable to provide LogLevel for '" + this.name + "'");
return this.logLevel;
}
/**
* Return if this is a custom level and cannot be represented by LogLevel.
*
* @return if this is a custom level
*/
public boolean isCustom() {
return this.logLevel == null;
}
/**
* Create a new LevelConfiguration instance of the given LogLevel.
*
* @param logLevel the log level
* @return a new LevelConfiguration instance
*/
public static LevelConfiguration of(LogLevel logLevel) {
Assert.notNull(logLevel, "'logLevel' must not be null");
return new LevelConfiguration(logLevel.name(), logLevel);
}
/**
* Create a new LevelConfiguration instance for a custom level name.
*
* @param name the log level name
* @return a new LevelConfiguration instance
*/
public static LevelConfiguration ofCustom(String name) {
Assert.hasText(name, "'name' must not be empty");
return new LevelConfiguration(name, null);
}
}
}Context passed to the LoggingSystem during initialization, providing access to the Spring Environment.
package org.springframework.boot.logging;
import org.jspecify.annotations.Nullable;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
/**
* Context passed to the LoggingSystem during initialization.
*
* @since 1.3.0
*/
public class LoggingInitializationContext {
/**
* Create a new LoggingInitializationContext instance.
*
* @param environment the Spring environment (can be null)
*/
public LoggingInitializationContext(@Nullable ConfigurableEnvironment environment) {
// Stores the environment for later access
}
/**
* Return the Spring environment if available.
*
* @return the Environment or null if not available
*/
public @Nullable Environment getEnvironment() {
return this.environment;
}
}Usage Notes:
LoggingSystem.initialize() methodsUtility to set system properties that can be used by log configuration files.
package org.springframework.boot.logging;
/**
* Utility to set system properties that can later be used by log configuration files.
*
* @since 2.0.0
*/
public class LoggingSystemProperties {
/**
* Create a new LoggingSystemProperties instance.
*
* @param environment the source environment
*/
public LoggingSystemProperties(Environment environment) {
this(environment, null);
}
/**
* Create a new LoggingSystemProperties instance.
*
* @param environment the source environment
* @param setter setter used to apply the property or null for system properties
* @since 2.4.2
*/
public LoggingSystemProperties(Environment environment,
BiConsumer<String, String> setter) {
this(environment, null, setter);
}
/**
* Create a new LoggingSystemProperties instance.
*
* @param environment the source environment
* @param defaultValueResolver function used to resolve default values or null
* @param setter setter used to apply the property or null for system properties
* @since 3.2.0
*/
public LoggingSystemProperties(Environment environment,
Function<String, String> defaultValueResolver,
BiConsumer<String, String> setter) {
Assert.notNull(environment, "'environment' must not be null");
this.environment = environment;
this.defaultValueResolver = (defaultValueResolver != null)
? defaultValueResolver : (name) -> null;
this.setter = (setter != null) ? setter : systemPropertySetter;
}
/**
* Returns the Console to use.
*
* @return the Console to use
* @since 3.5.0
*/
protected Console getConsole() {
return System.console();
}
/**
* Apply all properties.
*/
public final void apply() {
apply(null);
}
/**
* Apply all properties with the given log file.
*
* @param logFile the log file to use
*/
public final void apply(LogFile logFile) {
PropertyResolver resolver = getPropertyResolver();
apply(logFile, resolver);
}
/**
* Returns the default console charset.
*
* @return the default console charset
* @since 3.5.0
*/
protected Charset getDefaultConsoleCharset() {
Console console = getConsole();
return (console != null) ? console.charset() : Charset.defaultCharset();
}
/**
* Returns the default file charset.
*
* @return the default file charset
* @since 3.5.0
*/
protected Charset getDefaultFileCharset() {
return StandardCharsets.UTF_8;
}
}Usage Example:
// Configure logging properties
ConfigurableEnvironment environment = new StandardEnvironment();
LogFile logFile = LogFile.get(environment);
LoggingSystemProperties properties = new LoggingSystemProperties(environment);
properties.apply(logFile);
// Properties like PID, charset, patterns are now available as system properties
String pid = System.getProperty("PID");
String consoleCharset = System.getProperty("CONSOLE_CHARSET");Spring Boot allows you to define logger groups for controlling multiple loggers simultaneously.
A single logger group containing multiple logger members.
package org.springframework.boot.logging;
/**
* A single logger group.
*
* @since 2.2.0
*/
public final class LoggerGroup {
/**
* Return the name of the logger group.
*
* @return the group name
*/
public String getName() {
return this.name;
}
/**
* Return the members of the logger group.
*
* @return the group members
*/
public List<String> getMembers() {
return this.members;
}
/**
* Check if the group has any members.
*
* @return true if the group has members
*/
public boolean hasMembers() {
return !this.members.isEmpty();
}
/**
* Get the configured level for this group.
*
* @return the configured level or null
*/
public LogLevel getConfiguredLevel() {
return this.configuredLevel;
}
/**
* Configure the log level for all members of this group.
*
* @param level the level to configure
* @param configurer consumer to apply the level to each member
*/
public void configureLogLevel(LogLevel level,
BiConsumer<String, LogLevel> configurer) {
this.configuredLevel = level;
this.members.forEach((name) -> configurer.accept(name, level));
}
}Container for managing multiple logger groups.
package org.springframework.boot.logging;
/**
* Logger groups configured through the Spring Environment.
*
* @since 2.2.0
*/
public final class LoggerGroups implements Iterable<LoggerGroup> {
/**
* Create a new LoggerGroups instance.
*/
public LoggerGroups() {
}
/**
* Create a new LoggerGroups instance with the given group definitions.
*
* @param namesAndMembers map of group names to member logger names
*/
public LoggerGroups(Map<String, List<String>> namesAndMembers) {
putAll(namesAndMembers);
}
/**
* Add multiple groups to this collection.
*
* @param namesAndMembers map of group names to member logger names
*/
public void putAll(Map<String, List<String>> namesAndMembers) {
namesAndMembers.forEach(this::put);
}
/**
* Get a logger group by name.
*
* @param name the group name
* @return the logger group or null if not found
*/
public LoggerGroup get(String name) {
return this.groups.get(name);
}
/**
* Return an iterator over all logger groups.
*
* @return an iterator of logger groups
*/
@Override
public Iterator<LoggerGroup> iterator() {
return this.groups.values().iterator();
}
}Logger Groups Usage Example:
// Define logger groups in application.properties
// logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
// logging.level.tomcat=TRACE
// Programmatic usage
Map<String, List<String>> groupDefinitions = new HashMap<>();
groupDefinitions.put("web", List.of(
"org.springframework.web",
"org.springframework.web.servlet"
));
groupDefinitions.put("sql", List.of(
"org.hibernate.SQL",
"org.hibernate.type.descriptor.sql"
));
LoggerGroups loggerGroups = new LoggerGroups(groupDefinitions);
// Set log level for a group
LoggingSystem loggingSystem = LoggingSystem.get(getClass().getClassLoader());
LoggerGroup webGroup = loggerGroups.get("web");
if (webGroup != null) {
webGroup.configureLogLevel(LogLevel.DEBUG, loggingSystem::setLogLevel);
}Spring Boot provides support for formatting correlation IDs in log output based on W3C recommendations.
Utility for formatting correlation identifiers in log messages.
package org.springframework.boot.logging;
/**
* Utility class that can be used to format a correlation identifier for logging
* based on W3C recommendations.
*
* The formatter can be configured with a comma-separated list of names and the
* expected length of their resolved value. Each item should be specified in the
* form "<name>(length)". For example, "traceId(32),spanId(16)".
*
* @since 3.2.0
*/
public final class CorrelationIdFormatter {
/**
* Default CorrelationIdFormatter using traceId and spanId.
*/
public static final CorrelationIdFormatter DEFAULT =
CorrelationIdFormatter.of("traceId(32),spanId(16)");
/**
* Format a correlation from the values in the given resolver.
*
* @param resolver the resolver used to resolve named values
* @return a formatted correlation id
*/
public String format(UnaryOperator<String> resolver) {
StringBuilder result = new StringBuilder(this.blank.length());
formatTo(resolver, result);
return result.toString();
}
/**
* Format a correlation from the values in the given resolver and append it
* to the given Appendable.
*
* @param resolver the resolver used to resolve named values
* @param appendable the appendable for the formatted correlation id
*/
public void formatTo(UnaryOperator<String> resolver, Appendable appendable) {
// Formats as: [traceId-spanId] or blank if values not available
}
/**
* Create a new CorrelationIdFormatter instance from the given specification.
*
* @param spec a comma-separated specification
* @return a new CorrelationIdFormatter instance
*/
public static CorrelationIdFormatter of(String spec) {
try {
return (!StringUtils.hasText(spec)) ? DEFAULT : of(List.of(spec.split(",")));
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to parse correlation formatter spec '%s'".formatted(spec), ex);
}
}
/**
* Create a new CorrelationIdFormatter instance from the given specification.
*
* @param spec a pre-separated specification
* @return a new CorrelationIdFormatter instance
*/
public static CorrelationIdFormatter of(String[] spec) {
return of((spec != null) ? List.of(spec) : Collections.emptyList());
}
/**
* Create a new CorrelationIdFormatter instance from the given specification.
*
* @param spec a pre-separated specification
* @return a new CorrelationIdFormatter instance
*/
public static CorrelationIdFormatter of(Collection<String> spec) {
if (CollectionUtils.isEmpty(spec)) {
return DEFAULT;
}
List<Part> parts = spec.stream().map(Part::of).toList();
return new CorrelationIdFormatter(parts);
}
}Correlation ID Usage Example:
// Using correlation IDs with MDC
import org.slf4j.MDC;
import org.springframework.boot.logging.CorrelationIdFormatter;
// Set correlation values in MDC
MDC.put("traceId", "01234567890123456789012345678901");
MDC.put("spanId", "0123456789012345");
// Format correlation ID
CorrelationIdFormatter formatter = CorrelationIdFormatter.DEFAULT;
String formatted = formatter.format(MDC::get);
// Result: [01234567890123456789012345678901-0123456789012345]
// Custom correlation format
CorrelationIdFormatter custom = CorrelationIdFormatter.of("requestId(16),userId(8)");
MDC.put("requestId", "abc123def456");
MDC.put("userId", "user001");
String customFormatted = custom.format(MDC::get);
// Result: [abc123def456 -user001 ] (with padding to maintain fixed width)Deferred logging allows log messages to be buffered during early application startup and replayed later when the logging system is fully initialized.
Factory interface for creating deferred logs.
package org.springframework.boot.logging;
/**
* Factory that can be used to create multiple DeferredLog instances that will
* switch over when appropriate.
*
* @since 2.4.0
*/
@FunctionalInterface
public interface DeferredLogFactory {
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
default Log getLog(Class<?> destination) {
return getLog(() -> LogFactory.getLog(destination));
}
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
default Log getLog(Log destination) {
return getLog(() -> destination);
}
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
Log getLog(Supplier<Log> destination);
}The main class that implements deferred logging behavior.
package org.springframework.boot.logging;
/**
* Deferred Log that can be used to store messages that shouldn't be written
* until the logging system is fully initialized.
*
* @since 1.3.0
*/
public class DeferredLog implements Log {
/**
* Create a new DeferredLog instance.
*/
public DeferredLog() {
this.destinationSupplier = null;
this.lines = new Lines();
}
/**
* Switch from deferred logging to immediate logging to the specified destination.
*
* @param destination the new log destination
* @since 2.1.0
*/
public void switchTo(Class<?> destination) {
switchTo(LogFactory.getLog(destination));
}
/**
* Switch from deferred logging to immediate logging to the specified destination.
*
* @param destination the new log destination
* @since 2.1.0
*/
public void switchTo(Log destination) {
synchronized (this.lines) {
replayTo(destination);
this.destination = destination;
}
}
/**
* Replay deferred logging to the specified destination.
*
* @param destination the destination for the deferred log messages
*/
public void replayTo(Class<?> destination) {
replayTo(LogFactory.getLog(destination));
}
/**
* Replay deferred logging to the specified destination.
*
* @param destination the destination for the deferred log messages
*/
public void replayTo(Log destination) {
synchronized (this.lines) {
for (Line line : this.lines) {
line.getLevel().log(destination, line.getMessage(), line.getThrowable());
}
this.lines.clear();
}
}
/**
* Replay from a source log to a destination log when the source is deferred.
*
* @param source the source logger
* @param destination the destination logger class
* @return the destination
*/
public static Log replay(Log source, Class<?> destination) {
return replay(source, LogFactory.getLog(destination));
}
/**
* Replay from a source log to a destination log when the source is deferred.
*
* @param source the source logger
* @param destination the destination logger
* @return the destination
*/
public static Log replay(Log source, Log destination) {
if (source instanceof DeferredLog deferredLog) {
deferredLog.replayTo(destination);
}
return destination;
}
// All Log interface methods (isTraceEnabled, trace, debug, info, warn, error, fatal, etc.)
// are implemented to either defer or forward depending on switch state
}Implementation of DeferredLogFactory that manages a collection of deferred log instances.
package org.springframework.boot.logging;
/**
* A DeferredLogFactory implementation that manages a collection of DeferredLog instances.
*
* @since 2.4.0
*/
public class DeferredLogs implements DeferredLogFactory {
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
@Override
public Log getLog(Class<?> destination) {
return getLog(() -> LogFactory.getLog(destination));
}
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
@Override
public Log getLog(Log destination) {
return getLog(() -> destination);
}
/**
* Create a new DeferredLog for the given destination.
*
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when appropriate
*/
@Override
public Log getLog(Supplier<Log> destination) {
synchronized (this.lines) {
DeferredLog logger = new DeferredLog(destination, this.lines);
this.loggers.add(logger);
return logger;
}
}
/**
* Switch over all deferred logs to their supplied destination.
*/
public void switchOverAll() {
synchronized (this.lines) {
for (Line line : this.lines) {
line.getLevel().log(line.getDestination(),
line.getMessage(),
line.getThrowable());
}
for (DeferredLog logger : this.loggers) {
logger.switchOver();
}
this.lines.clear();
}
}
}Usage Example:
public class MyApplication {
private static final DeferredLogs deferredLogs = new DeferredLogs();
private static final Log logger = deferredLogs.getLog(MyApplication.class);
public static void main(String[] args) {
// These messages are buffered
logger.info("Application starting...");
logger.debug("Early initialization phase");
// Initialize the application
SpringApplication app = new SpringApplication(MyApplication.class);
ConfigurableApplicationContext context = app.run(args);
// Now replay all deferred log messages
deferredLogs.switchOverAll();
// This message goes directly to the logging system
logger.info("Application started successfully");
}
}Spring Boot provides comprehensive support for Logback, the default logging framework.
Logback implementation of the LoggingSystem.
package org.springframework.boot.logging.logback;
/**
* LoggingSystem for Logback.
*
* @since 1.0.0
*/
public class LogbackLoggingSystem extends AbstractLoggingSystem
implements BeanFactoryInitializationAotProcessor {
/**
* Create a new LogbackLoggingSystem instance.
*
* @param classLoader the class loader
*/
public LogbackLoggingSystem(ClassLoader classLoader) {
super(classLoader);
}
@Override
public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) {
return new LogbackLoggingSystemProperties(environment,
getDefaultValueResolver(environment),
null);
}
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
configureJdkLoggingBridgeHandler();
loggerContext.getTurboFilterList().add(SUPPRESS_ALL_FILTER);
}
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation,
LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
putInitializationContextObjects(loggerContext, initializationContext);
if (isAlreadyInitialized(loggerContext)) {
return;
}
if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {
super.initialize(initializationContext, configLocation, logFile);
}
loggerContext.getTurboFilterList().remove(SUPPRESS_ALL_FILTER);
markAsInitialized(loggerContext);
}
@Override
public List<LoggerConfiguration> getLoggerConfigurations() {
List<LoggerConfiguration> result = new ArrayList<>();
for (ch.qos.logback.classic.Logger logger : getLoggerContext().getLoggerList()) {
result.add(getLoggerConfiguration(logger));
}
result.sort(CONFIGURATION_COMPARATOR);
return result;
}
@Override
public LoggerConfiguration getLoggerConfiguration(String loggerName) {
String name = getLoggerName(loggerName);
LoggerContext loggerContext = getLoggerContext();
return getLoggerConfiguration(loggerContext.exists(name));
}
@Override
public Set<LogLevel> getSupportedLogLevels() {
return LEVELS.getSupported();
}
@Override
public void setLogLevel(String loggerName, LogLevel level) {
ch.qos.logback.classic.Logger logger = getLogger(loggerName);
if (logger != null) {
logger.setLevel(LEVELS.convertSystemToNative(level));
}
}
@Override
public Runnable getShutdownHandler() {
return () -> getLoggerContext().stop();
}
/**
* LoggingSystemFactory that returns LogbackLoggingSystem if possible.
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 1024)
public static class Factory implements LoggingSystemFactory {
@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
if (PRESENT) {
return new LogbackLoggingSystem(classLoader);
}
return null;
}
}
}Logback encoder for structured logging.
package org.springframework.boot.logging.logback;
/**
* Logback encoder for structured logging.
*
* @since 3.4.0
*/
public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
/**
* Set the format for structured logging.
*
* @param format the format (e.g., "ecs", "gelf", "logstash", or a fully-qualified class name)
*/
public void setFormat(String format) {
this.format = format;
}
/**
* Set the charset for encoding.
*
* @param charset the charset to use
*/
public void setCharset(Charset charset) {
this.charset = charset;
}
@Override
public void start() {
Assert.state(this.format != null, "Format has not been set");
this.formatter = createFormatter(this.format);
super.start();
this.throwableProxyConverter.start();
}
@Override
public byte[] encode(ILoggingEvent event) {
Assert.state(this.formatter != null,
"formatter must not be null. Make sure to call start() before this method");
return this.formatter.formatAsBytes(event,
(this.charset != null) ? this.charset : StandardCharsets.UTF_8);
}
@Override
public void stop() {
this.throwableProxyConverter.stop();
super.stop();
}
}Logback Configuration Example:
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder">
<format>ecs</format>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_FILE}</file>
<encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder">
<format>logstash</format>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>Spring Boot provides custom Logback converters for enhanced pattern layout functionality.
Converter that applies ANSI color codes to log output based on log level or explicit color specification.
package org.springframework.boot.logging.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.CompositeConverter;
/**
* Logback CompositeConverter to color output using AnsiOutput class.
* Colors can be specified explicitly or applied automatically based on log level:
* - ERROR: RED
* - WARN: YELLOW
* - INFO and below: GREEN
*
* Supported color names: black, red, green, yellow, blue, magenta, cyan, white,
* bright_black, bright_red, bright_green, bright_yellow, bright_blue,
* bright_magenta, bright_cyan, bright_white, faint
*
* @since 1.0.0
*/
public class ColorConverter extends CompositeConverter<ILoggingEvent> {
@Override
protected String transform(ILoggingEvent event, String in);
protected String toAnsiString(String in, AnsiElement element);
}Usage in logback-spring.xml:
<configuration>
<conversionRule conversionWord="clr"
converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Auto-coloring based on level -->
<pattern>%clr(%d{HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %m%n</pattern>
<!-- Explicit color specification -->
<pattern>%clr(%logger{36}){blue} : %m%n</pattern>
</encoder>
</appender>
</configuration>Examples:
<!-- Level-based coloring -->
%clr(%p) <!-- ERROR=red, WARN=yellow, INFO/DEBUG=green -->
<!-- Explicit colors -->
%clr(%logger){blue}
%clr(%d{ISO8601}){faint}
%clr([%thread]){magenta}
%clr(---){faint}
<!-- Common pattern with colors -->
<pattern>
%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint}
%clr(%5p)
%clr(${PID:- }){magenta}
%clr(---){faint}
%clr([%15.15t]){faint}
%clr(%-40.40logger{39}){cyan}
%clr(:){faint} %m%n
</pattern>Converter that formats correlation IDs from MDC for distributed tracing.
package org.springframework.boot.logging.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.DynamicConverter;
/**
* Logback DynamicConverter to convert a CorrelationIdFormatter pattern into
* formatted output using data from the MDC and Environment.
*
* Supports formatting correlation IDs for distributed tracing including:
* - Trace ID
* - Span ID
* - Parent ID (optional)
*
* @since 3.2.0
*/
public class CorrelationIdConverter extends DynamicConverter<ILoggingEvent> {
@Override
public void start();
@Override
public void stop();
@Override
public String convert(ILoggingEvent event);
}Usage in logback-spring.xml:
<configuration>
<conversionRule conversionWord="correlationId"
converterClass="org.springframework.boot.logging.logback.CorrelationIdConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Include correlation ID in logs -->
<pattern>%d{HH:mm:ss.SSS} %correlationId %5p --- %m%n</pattern>
<!-- Custom pattern with trace/span IDs -->
<pattern>%d{HH:mm:ss.SSS} [%correlationId] %m%n</pattern>
</encoder>
</appender>
</configuration>Converter that adds whitespace before exception stack traces for better readability.
package org.springframework.boot.logging.logback;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.IThrowableProxy;
/**
* ThrowableProxyConverter that adds whitespace before the stack trace.
* Improves readability by indenting exception traces.
*
* @since 1.0.0
*/
public class WhitespaceThrowableProxyConverter extends ThrowableProxyConverter {
@Override
protected String throwableProxyToString(IThrowableProxy tp);
}Usage in logback-spring.xml:
<configuration>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Exception with leading whitespace -->
<pattern>%d{HH:mm:ss.SSS} %5p --- %m%wex%n</pattern>
</encoder>
</appender>
</configuration>Extended version that adds whitespace and includes extended stack trace information.
package org.springframework.boot.logging.logback;
import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter;
import ch.qos.logback.classic.spi.IThrowableProxy;
/**
* ExtendedThrowableProxyConverter that adds whitespace before the stack trace.
* Extends ThrowableProxyConverter to include packaging information from JAR files.
*
* @since 1.0.0
*/
public class ExtendedWhitespaceThrowableProxyConverter extends ExtendedThrowableProxyConverter {
@Override
protected String throwableProxyToString(IThrowableProxy tp);
}Usage in logback-spring.xml:
<configuration>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<encoder>
<!-- Exception with packaging info and whitespace -->
<pattern>%d{HH:mm:ss.SSS} %5p --- %m%wEx%n</pattern>
</encoder>
</appender>
</configuration>Converter that encloses output in square brackets.
package org.springframework.boot.logging.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.CompositeConverter;
/**
* CompositeConverter that encloses the converted result in square brackets.
* Useful for formatting optional values that should be bracketed when present.
*
* @since 2.0.0
*/
public class EnclosedInSquareBracketsConverter extends CompositeConverter<ILoggingEvent> {
@Override
protected String transform(ILoggingEvent event, String in);
}Usage in logback-spring.xml:
<configuration>
<conversionRule conversionWord="enclose"
converterClass="org.springframework.boot.logging.logback.EnclosedInSquareBracketsConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Enclose thread name in brackets -->
<pattern>%d{HH:mm:ss.SSS} %enclose(%thread) %m%n</pattern>
<!-- Output: 14:23:45.123 [main] Log message -->
<!-- Enclose correlation ID -->
<pattern>%enclose(%correlationId) %m%n</pattern>
<!-- Output: [trace-id-123] Log message -->
</encoder>
</appender>
</configuration>Combining all converters for production-ready logging:
<configuration>
<!-- Register all Spring Boot converters -->
<conversionRule conversionWord="clr"
converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="correlationId"
converterClass="org.springframework.boot.logging.logback.CorrelationIdConverter" />
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %clr([%correlationId]){blue} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>Output Example:
2024-01-15 14:23:45.123 INFO 12345 --- [main] c.e.MyApplication : [trace-abc123] Application started
2024-01-15 14:23:45.456 WARN 12345 --- [pool-1] c.e.MyService : [trace-abc123] Warning message
2024-01-15 14:23:45.789 ERROR 12345 --- [pool-2] c.e.MyController : [trace-def456] Error occurred
java.lang.RuntimeException: Something went wrong
at com.example.MyController.process(MyController.java:42)
... (stack trace continues)Spring Boot provides full support for Log4j2 as an alternative logging framework.
Log4j2 implementation of the LoggingSystem.
package org.springframework.boot.logging.log4j2;
/**
* LoggingSystem for Log4j2.
*
* @since 1.2.0
*/
public class Log4J2LoggingSystem extends AbstractLoggingSystem {
/**
* Environment key for storing Spring Environment in LoggerContext.
*/
static final String ENVIRONMENT_KEY =
Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, "environment");
/**
* Create a new Log4J2LoggingSystem instance.
*
* @param classLoader the class loader
* @param loggerContext the LoggerContext to use
*/
Log4J2LoggingSystem(ClassLoader classLoader, LoggerContext loggerContext) {
super(classLoader);
this.loggerContext = loggerContext;
}
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
if (!configureJdkLoggingBridgeHandler()) {
super.beforeInitialize();
}
loggerContext.getConfiguration().addFilter(FILTER);
}
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation,
LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
StatusConsoleListener listener = new StatusConsoleListener(Level.WARN);
StatusLogger.getLogger().registerListener(listener);
loggerContext.putObject(STATUS_LISTENER_KEY, listener);
Environment environment = initializationContext.getEnvironment();
if (environment != null) {
loggerContext.putObject(ENVIRONMENT_KEY, environment);
Log4J2LoggingSystem.propertySource.setEnvironment(environment);
PropertiesUtil.getProperties().addPropertySource(propertySource);
}
loggerContext.getConfiguration().removeFilter(FILTER);
super.initialize(initializationContext, configLocation, logFile);
markAsInitialized(loggerContext);
}
@Override
public Set<LogLevel> getSupportedLogLevels() {
return LEVELS.getSupported();
}
@Override
public void setLogLevel(String loggerName, LogLevel logLevel) {
setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
}
@Override
public List<LoggerConfiguration> getLoggerConfigurations() {
List<LoggerConfiguration> result = new ArrayList<>();
getAllLoggers().forEach((name, loggerConfig) ->
result.add(convertLoggerConfig(name, loggerConfig)));
result.sort(CONFIGURATION_COMPARATOR);
return result;
}
@Override
public LoggerConfiguration getLoggerConfiguration(String loggerName) {
LoggerConfig loggerConfig = getAllLoggers().get(loggerName);
return (loggerConfig != null)
? convertLoggerConfig(loggerName, loggerConfig)
: null;
}
@Override
public Runnable getShutdownHandler() {
return () -> getLoggerContext().stop();
}
/**
* Get the Spring Environment attached to the given LoggerContext.
*
* @param loggerContext the logger context
* @return the Spring Environment or null
* @since 3.0.0
*/
public static Environment getEnvironment(LoggerContext loggerContext) {
return (Environment) ((loggerContext != null)
? loggerContext.getObject(ENVIRONMENT_KEY)
: null);
}
/**
* LoggingSystemFactory that returns Log4J2LoggingSystem if possible.
*/
@Order(0)
public static class Factory implements LoggingSystemFactory {
@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
if (PRESENT) {
org.apache.logging.log4j.spi.LoggerContext spiLoggerContext =
LogManager.getContext(classLoader, false);
if (spiLoggerContext instanceof LoggerContext coreLoggerContext) {
return new Log4J2LoggingSystem(classLoader, coreLoggerContext);
}
}
return null;
}
}
}Log4j2 layout for structured logging.
package org.springframework.boot.logging.log4j2;
/**
* Log4j2 Layout for structured logging.
*/
@Plugin(name = "StructuredLogLayout",
category = Node.CATEGORY,
elementType = Layout.ELEMENT_TYPE)
final class StructuredLogLayout extends AbstractStringLayout {
private StructuredLogLayout(Charset charset,
StructuredLogFormatter<LogEvent> formatter) {
super(charset);
Assert.notNull(formatter, "'formatter' must not be null");
this.formatter = formatter;
}
@Override
public String toSerializable(LogEvent event) {
return this.formatter.format(event);
}
@Override
public byte[] toByteArray(LogEvent event) {
return this.formatter.formatAsBytes(event,
(getCharset() != null) ? getCharset() : StandardCharsets.UTF_8);
}
/**
* Create a new builder for this layout.
*
* @return a new Builder instance
*/
@PluginBuilderFactory
static StructuredLogLayout.Builder newBuilder() {
return new StructuredLogLayout.Builder();
}
static final class Builder implements org.apache.logging.log4j.core.util.Builder<StructuredLogLayout> {
@PluginLoggerContext
private LoggerContext loggerContext;
@PluginBuilderAttribute
private String format;
@PluginBuilderAttribute
private String charset = StandardCharsets.UTF_8.name();
/**
* Set the format for structured logging.
*
* @param format the format
* @return this builder
*/
public Builder setFormat(String format) {
this.format = format;
return this;
}
/**
* Set the charset for encoding.
*
* @param charset the charset name
* @return this builder
*/
public Builder setCharset(String charset) {
this.charset = charset;
return this;
}
@Override
public StructuredLogLayout build() {
Charset charset = Charset.forName(this.charset);
Environment environment = Log4J2LoggingSystem.getEnvironment(this.loggerContext);
Assert.state(environment != null,
"Unable to find Spring Environment in logger context");
StructuredLogFormatter<LogEvent> formatter =
new StructuredLogFormatterFactory<>(LogEvent.class, environment,
null, this::addCommonFormatters).get(this.format);
return new StructuredLogLayout(charset, formatter);
}
}
}Log4j2 Configuration Example:
<!-- log4j2-spring.xml -->
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<StructuredLogLayout format="ecs" charset="UTF-8"/>
</Console>
<File name="File" fileName="${sys:LOG_FILE}">
<StructuredLogLayout format="gelf"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>Spring Boot also supports Java's built-in logging framework.
Java Util Logging implementation of the LoggingSystem.
package org.springframework.boot.logging.java;
/**
* LoggingSystem for java.util.logging.
*
* @since 1.0.0
*/
public class JavaLoggingSystem extends AbstractLoggingSystem {
/**
* Create a new JavaLoggingSystem instance.
*
* @param classLoader the class loader
*/
public JavaLoggingSystem(ClassLoader classLoader) {
super(classLoader);
}
@Override
protected String[] getStandardConfigLocations() {
return new String[] { "logging.properties" };
}
@Override
public void beforeInitialize() {
super.beforeInitialize();
Logger.getLogger("").setLevel(Level.SEVERE);
}
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext,
LogFile logFile) {
if (logFile != null) {
loadConfiguration(getPackagedConfigFile("logging-file.properties"), logFile);
}
else {
loadConfiguration(getPackagedConfigFile("logging.properties"), null);
}
}
@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext,
String location,
LogFile logFile) {
loadConfiguration(location, logFile);
}
/**
* Load configuration from the given location.
*
* @param location the configuration location
* @param logFile the log file or null
*/
protected void loadConfiguration(String location, LogFile logFile) {
Assert.notNull(location, "'location' must not be null");
try {
Resource resource = ApplicationResourceLoader.get().getResource(location);
String configuration = FileCopyUtils.copyToString(
new InputStreamReader(resource.getInputStream()));
if (logFile != null) {
configuration = configuration.replace("${LOG_FILE}",
StringUtils.cleanPath(logFile.toString()));
}
LogManager.getLogManager().readConfiguration(
new ByteArrayInputStream(configuration.getBytes()));
}
catch (Exception ex) {
throw new IllegalStateException(
"Could not initialize Java logging from " + location, ex);
}
}
@Override
public Set<LogLevel> getSupportedLogLevels() {
return LEVELS.getSupported();
}
@Override
public void setLogLevel(String loggerName, LogLevel level) {
if (loggerName == null || ROOT_LOGGER_NAME.equals(loggerName)) {
loggerName = "";
}
Logger logger = Logger.getLogger(loggerName);
if (logger != null) {
this.configuredLoggers.add(logger);
logger.setLevel(LEVELS.convertSystemToNative(level));
}
}
@Override
public List<LoggerConfiguration> getLoggerConfigurations() {
List<LoggerConfiguration> result = new ArrayList<>();
Enumeration<String> names = LogManager.getLogManager().getLoggerNames();
while (names.hasMoreElements()) {
result.add(getLoggerConfiguration(names.nextElement()));
}
result.sort(CONFIGURATION_COMPARATOR);
return Collections.unmodifiableList(result);
}
@Override
public LoggerConfiguration getLoggerConfiguration(String loggerName) {
Logger logger = Logger.getLogger(loggerName);
if (logger == null) {
return null;
}
LogLevel level = LEVELS.convertNativeToSystem(logger.getLevel());
LogLevel effectiveLevel = LEVELS.convertNativeToSystem(getEffectiveLevel(logger));
String name = (StringUtils.hasLength(logger.getName())
? logger.getName()
: ROOT_LOGGER_NAME);
Assert.state(effectiveLevel != null, "effectiveLevel must not be null");
return new LoggerConfiguration(name, level, effectiveLevel);
}
@Override
public Runnable getShutdownHandler() {
return () -> LogManager.getLogManager().reset();
}
@Override
public void cleanUp() {
this.configuredLoggers.clear();
}
/**
* LoggingSystemFactory that returns JavaLoggingSystem if possible.
*/
@Order(Ordered.LOWEST_PRECEDENCE - 1024)
public static class Factory implements LoggingSystemFactory {
@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
if (PRESENT) {
return new JavaLoggingSystem(classLoader);
}
return null;
}
}
}Java Logging Configuration Example:
# logging.properties
handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler
.level=INFO
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.level=ALL
java.util.logging.FileHandler.pattern=${LOG_FILE}
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
com.example.myapp.level=DEBUGSpring Boot 3.4+ introduces comprehensive support for structured logging formats.
Core interface for formatting log events as structured data.
package org.springframework.boot.logging.structured;
/**
* Formats a log event to a structured log message.
*
* Implementing classes can declare the following parameter types in the constructor:
* - Environment
* - StructuredLoggingJsonMembersCustomizer
* - StructuredLoggingJsonMembersCustomizer.Builder
* - StackTracePrinter (may be null)
* - ContextPairs
*
* When using Logback, implementing classes can also use:
* - ch.qos.logback.classic.pattern.ThrowableProxyConverter
*
* @param <E> the log event type
* @since 3.4.0
*/
@FunctionalInterface
public interface StructuredLogFormatter<E> {
/**
* Formats the given log event to a String.
*
* @param event the log event to write
* @return the formatted log event String
*/
String format(E event);
/**
* Formats the given log event to a byte array.
*
* @param event the log event to write
* @param charset the charset
* @return the formatted log event bytes
*/
default byte[] formatAsBytes(E event, Charset charset) {
return format(event).getBytes(charset);
}
}Enumeration of common structured log formats supported by Spring Boot.
package org.springframework.boot.logging.structured;
/**
* Common structured log formats supported by Spring Boot.
*
* @since 3.4.0
*/
public enum CommonStructuredLogFormat {
/**
* Elastic Common Schema (ECS) log format.
*
* @see <a href="https://www.elastic.co/guide/en/ecs/current/ecs-log.html">ECS Documentation</a>
*/
ELASTIC_COMMON_SCHEMA("ecs"),
/**
* Graylog Extended Log Format (GELF) log format.
*
* @see <a href="https://go2docs.graylog.org/current/getting_in_log_data/gelf.html">GELF Documentation</a>
*/
GRAYLOG_EXTENDED_LOG_FORMAT("gelf"),
/**
* Logstash log format.
*
* @see <a href="https://github.com/logfellow/logstash-logback-encoder">Logstash Encoder</a>
*/
LOGSTASH("logstash");
/**
* Return the ID for this format.
*
* @return the format identifier
*/
String getId() {
return this.id;
}
/**
* Find the CommonStructuredLogFormat for the given ID.
*
* @param id the format identifier
* @return the associated CommonStructuredLogFormat or null
*/
static CommonStructuredLogFormat forId(String id) {
for (CommonStructuredLogFormat candidate : values()) {
if (candidate.getId().equalsIgnoreCase(id)) {
return candidate;
}
}
return null;
}
}Base class for JSON-based structured log formatters.
package org.springframework.boot.logging.structured;
/**
* Base class for StructuredLogFormatter implementations that generates JSON
* using a JsonWriter.
*
* @param <E> the log event type
* @since 3.4.0
*/
public abstract class JsonWriterStructuredLogFormatter<E>
implements StructuredLogFormatter<E> {
/**
* Create a new JsonWriterStructuredLogFormatter instance with the given members.
*
* @param members a consumer which should configure the members
* @param customizer an optional customizer to apply
*/
protected JsonWriterStructuredLogFormatter(
Consumer<Members<E>> members,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
this(JsonWriter.of(customized(members, customizer)).withNewLineAtEnd());
}
/**
* Create a new JsonWriterStructuredLogFormatter instance with the given JsonWriter.
*
* @param jsonWriter the JsonWriter
*/
protected JsonWriterStructuredLogFormatter(JsonWriter<E> jsonWriter) {
this.jsonWriter = jsonWriter;
}
@Override
public String format(E event) {
return this.jsonWriter.writeToString(event);
}
@Override
public byte[] formatAsBytes(E event, Charset charset) {
return this.jsonWriter.write(event).toByteArray(charset);
}
}Factory for creating structured log formatter instances.
package org.springframework.boot.logging.structured;
/**
* Factory that can be used to create a fully instantiated StructuredLogFormatter
* for either a common format ID or a fully-qualified class name.
*
* @param <E> the log event type
* @since 3.4.0
*/
public class StructuredLogFormatterFactory<E> {
/**
* Create a new StructuredLogFormatterFactory instance.
*
* @param logEventType the log event type
* @param environment the Spring Environment
* @param availableParameters callback used to configure available parameters
* @param commonFormatters callback used to define supported common formatters
*/
public StructuredLogFormatterFactory(
Class<E> logEventType,
Environment environment,
Consumer<AvailableParameters> availableParameters,
Consumer<CommonFormatters<E>> commonFormatters) {
this(SpringFactoriesLoader.forDefaultResourceLocation(),
logEventType, environment, availableParameters, commonFormatters);
}
/**
* Get a new StructuredLogFormatter instance for the specified format.
*
* @param format the format requested (either a CommonStructuredLogFormat ID
* or a fully-qualified class name)
* @return a new StructuredLogFormatter instance
* @throws IllegalArgumentException if the format is unknown
*/
public StructuredLogFormatter<E> get(String format) {
StructuredLogFormatter<E> formatter =
this.commonFormatters.get(this.instantiator, format);
formatter = (formatter != null) ? formatter : getUsingClassName(format);
if (formatter != null) {
return formatter;
}
throw new IllegalArgumentException(
"Unknown format '%s'. Values can be a valid fully-qualified class name " +
"or one of the common formats: %s"
.formatted(format, this.commonFormatters.getCommonNames()));
}
/**
* Callback used to configure the CommonFormatterFactory to use for a given
* CommonStructuredLogFormat.
*
* @param <E> the log event type
*/
public static class CommonFormatters<E> {
/**
* Add the factory that should be used for the given CommonStructuredLogFormat.
*
* @param format the common structured log format
* @param factory the factory to use
*/
public void add(CommonStructuredLogFormat format,
CommonFormatterFactory<E> factory) {
this.factories.put(format, factory);
}
}
/**
* Factory used to create a StructuredLogFormatter for a given
* CommonStructuredLogFormat.
*
* @param <E> the log event type
*/
@FunctionalInterface
public interface CommonFormatterFactory<E> {
/**
* Create the StructuredLogFormatter instance.
*
* @param instantiator instantiator that can be used to obtain arguments
* @return a new StructuredLogFormatter instance
*/
StructuredLogFormatter<E> createFormatter(Instantiator<?> instantiator);
}
}Functional interface for printing stack traces of Throwables with customizable formatting. Provides a flexible API for generating stack trace strings with various options for filtering, formatting, and output control.
package org.springframework.boot.logging;
import java.io.IOException;
/**
* Interface that can be used to print the stack trace of a Throwable.
*
* @since 3.5.0
* @see StandardStackTracePrinter
*/
@FunctionalInterface
public interface StackTracePrinter {
/**
* Return a String containing the printed stack trace for a given Throwable.
*
* @param throwable the throwable that should have its stack trace printed
* @return the stack trace string
*/
default String printStackTraceToString(Throwable throwable) {
// Default implementation uses printStackTrace with StringBuilder
}
/**
* Prints a stack trace for the given Throwable.
*
* @param throwable the throwable that should have its stack trace printed
* @param out the destination to write output
* @throws IOException on IO error
*/
void printStackTrace(Throwable throwable, Appendable out) throws IOException;
}Standard implementation of StackTracePrinter that produces output similar to Throwable.printStackTrace() but with extensive customization options including filtering, formatting, hash generation, and output length limits.
package org.springframework.boot.logging;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
/**
* StackTracePrinter that prints a standard form stack trace with customization options.
*
* @since 3.5.0
*/
public final class StandardStackTracePrinter implements StackTracePrinter {
// Factory Methods
/**
* Return a StandardStackTracePrinter that prints the stack trace with the
* root exception last (same as Throwable.printStackTrace()).
*
* @return a StandardStackTracePrinter that prints root last
*/
public static StandardStackTracePrinter rootLast() { }
/**
* Return a StandardStackTracePrinter that prints the stack trace with the
* root exception first (opposite of Throwable.printStackTrace()).
*
* @return a StandardStackTracePrinter that prints root first
*/
public static StandardStackTracePrinter rootFirst() { }
// Customization Methods (all return new instance)
/**
* Return a new printer that will print all common frames rather than
* replacing them with "... N more".
*
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withCommonFrames() { }
/**
* Return a new printer that will not print suppressed items.
*
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withoutSuppressed() { }
/**
* Return a new printer that uses ellipses to truncate output longer
* than the specified length.
*
* @param maximumLength the maximum length that can be printed
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withMaximumLength(int maximumLength) { }
/**
* Return a new printer that filters frames deeper than the specified maximum.
*
* @param maximumThrowableDepth the maximum throwable depth
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withMaximumThrowableDepth(int maximumThrowableDepth) { }
/**
* Return a new printer that only includes throwables matching the predicate.
*
* @param predicate the predicate used to filter the throwable
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withFilter(Predicate<Throwable> predicate) { }
/**
* Return a new printer that only includes frames matching the predicate.
*
* @param predicate the predicate used to filter frames (index, element)
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withFrameFilter(
BiPredicate<Integer, StackTraceElement> predicate) { }
/**
* Return a new printer that uses the specified line separator.
*
* @param lineSeparator the line separator to use
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withLineSeparator(String lineSeparator) { }
/**
* Return a new printer that uses the specified formatter for throwables.
*
* @param formatter the formatter to use
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withFormatter(
Function<Throwable, String> formatter) { }
/**
* Return a new printer that uses the specified formatter for frames.
*
* @param frameFormatter the frame formatter to use
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withFrameFormatter(
Function<StackTraceElement, String> frameFormatter) { }
/**
* Return a new printer that generates and prints hashes for each stacktrace.
*
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withHashes() { }
/**
* Return a new printer that changes if hashes should be generated.
*
* @param hashes if hashes should be added
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withHashes(boolean hashes) { }
/**
* Return a new printer with a custom frame hasher for generating hashes.
*
* @param frameHasher the frame hasher (null to disable hashes)
* @return a new StandardStackTracePrinter instance
*/
public StandardStackTracePrinter withHashes(
ToIntFunction<StackTraceElement> frameHasher) { }
}Usage Examples:
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
// Basic usage - root last (standard order)
StackTracePrinter printer = StandardStackTracePrinter.rootLast();
String stackTrace = printer.printStackTraceToString(exception);
// Root first order (reverse)
StackTracePrinter rootFirst = StandardStackTracePrinter.rootFirst();
// With hash codes for deduplication
StackTracePrinter withHashes = StandardStackTracePrinter.rootLast()
.withHashes();
// Limit output length
StackTracePrinter limited = StandardStackTracePrinter.rootLast()
.withMaximumLength(5000);
// Filter specific exception types
StackTracePrinter filtered = StandardStackTracePrinter.rootLast()
.withFilter(t -> !(t instanceof IllegalArgumentException));
// Limit stack depth
StackTracePrinter shallow = StandardStackTracePrinter.rootLast()
.withMaximumThrowableDepth(10);
// Filter frames by package
StackTracePrinter packageFiltered = StandardStackTracePrinter.rootLast()
.withFrameFilter((index, element) ->
!element.getClassName().startsWith("com.sun."));
// Hide suppressed exceptions
StackTracePrinter noSuppressed = StandardStackTracePrinter.rootLast()
.withoutSuppressed();
// Custom formatting
StackTracePrinter custom = StandardStackTracePrinter.rootLast()
.withFormatter(t -> t.getClass().getSimpleName() + ": " + t.getMessage())
.withFrameFormatter(frame -> " at " + frame.getClassName() +
"." + frame.getMethodName());
// Production logging: compact with hashes, limited length
StackTracePrinter production = StandardStackTracePrinter.rootLast()
.withHashes()
.withMaximumLength(10000)
.withMaximumThrowableDepth(25)
.withoutSuppressed();Thread Safety: StackTracePrinter instances are immutable and thread-safe. All customization methods return new instances.
Common Use Cases:
Configuration properties for customizing structured logging JSON output.
Record for properties that customize structured logging JSON output.
package org.springframework.boot.logging.structured;
/**
* Properties that can be used to customize structured logging JSON.
*
* @param include the paths that should be included. An empty set includes all names
* @param exclude the paths that should be excluded. An empty set excludes nothing
* @param rename a map of path to replacement names
* @param add a map of additional elements
* @param stackTrace stack trace properties
* @param customizer the fully qualified names of StructuredLoggingJsonMembersCustomizer implementations
* @param context context specific properties
* @since 3.4.0
*/
record StructuredLoggingJsonProperties(
Set<String> include,
Set<String> exclude,
Map<String, String> rename,
Map<String, String> add,
StackTrace stackTrace,
Context context,
Set<Class<? extends StructuredLoggingJsonMembersCustomizer<?>>> customizer) {
/**
* Get StructuredLoggingJsonProperties from environment.
*
* @param environment the Spring Environment
* @return the bound properties or null
*/
static StructuredLoggingJsonProperties get(Environment environment);
/**
* Stack trace printing properties.
*
* @param printer the name of the printer to use. Can be null, "standard",
* "logging-system", or fully-qualified class name
* @param root the root ordering (root first or root last)
* @param maxLength the maximum length to print
* @param maxThrowableDepth the maximum throwable depth to print
* @param includeCommonFrames whether common frames should be included
* @param includeHashes whether stack trace hashes should be included
*/
record StackTrace(
String printer,
Root root,
Integer maxLength,
Integer maxThrowableDepth,
Boolean includeCommonFrames,
Boolean includeHashes) {
/**
* Root ordering for stack traces.
*/
enum Root {
/** Root cause last (default) */
LAST,
/** Root cause first */
FIRST
}
}
/**
* Properties that influence context values (usually elements propagated from MDC).
*
* @param include if context elements should be included
* @param prefix the prefix to use for context elements
* @since 3.5.0
*/
record Context(boolean include, String prefix);
}Configuration Example:
# JSON member configuration
logging.structured.json.include=timestamp,level,message
logging.structured.json.exclude=password,secret
logging.structured.json.rename.level=severity
logging.structured.json.add.environment=production
# Stack trace configuration
logging.structured.json.stack-trace.printer=standard
logging.structured.json.stack-trace.root=first
logging.structured.json.stack-trace.max-length=1000
logging.structured.json.stack-trace.include-common-frames=false
logging.structured.json.stack-trace.include-hashes=true
# Context (MDC) configuration
logging.structured.json.context.include=true
logging.structured.json.context.prefix=context.Properties for Elastic Common Schema (ECS) structured logging format.
package org.springframework.boot.logging.structured;
/**
* Properties for Elastic Common Schema structured logging.
*
* @param service service details
* @since 3.4.0
*/
public record ElasticCommonSchemaProperties(Service service) {
/**
* Add JsonWriter members for the service.
*
* @param members the members to add to
*/
public void jsonMembers(JsonWriter.Members<?> members);
/**
* Get ElasticCommonSchemaProperties from environment.
*
* @param environment the source environment
* @return a new ElasticCommonSchemaProperties instance
*/
public static ElasticCommonSchemaProperties get(Environment environment);
/**
* Service details for ECS.
*
* @param name the application name
* @param version the version of the application
* @param environment the name of the environment the application is running in
* @param nodeName the name of the node the application is running on
*/
public record Service(
String name,
String version,
String environment,
String nodeName);
}Configuration Example:
# ECS service configuration
logging.structured.ecs.service.name=my-application
logging.structured.ecs.service.version=1.0.0
logging.structured.ecs.service.environment=production
logging.structured.ecs.service.node-name=app-server-01Properties for Graylog Extended Log Format (GELF) structured logging.
package org.springframework.boot.logging.structured;
/**
* Properties for Graylog Extended Log Format structured logging.
*
* @param host the application name
* @param service service details
* @since 3.4.0
*/
public record GraylogExtendedLogFormatProperties(String host, Service service) {
/**
* Add JsonWriter members for the service.
*
* @param members the members to add to
*/
public void jsonMembers(JsonWriter.Members<?> members);
/**
* Get GraylogExtendedLogFormatProperties from environment.
*
* @param environment the source environment
* @return a new GraylogExtendedLogFormatProperties instance
*/
public static GraylogExtendedLogFormatProperties get(Environment environment);
/**
* Service details for GELF.
*
* @param version the version of the application
*/
public record Service(String version);
}Configuration Example:
# GELF service configuration
logging.structured.gelf.host=my-application
logging.structured.gelf.service.version=1.0.0Helper for adding JSON pairs from context data (typically logger MDC) in structured logging.
package org.springframework.boot.logging.structured;
/**
* Helper that can be used to add JSON pairs from context data (typically the logger MDC)
* in the correct location (or drop them altogether).
*
* @since 3.5.0
*/
public class ContextPairs {
/**
* Add pairs using flat naming.
*
* @param <T> the item type
* @param delimiter the delimiter used if there is a prefix
* @param pairs callback to add all the pairs
* @return a BiConsumer for use with the JsonWriter
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> flat(
String delimiter,
Consumer<Pairs<T>> pairs);
/**
* Add pairs using flat naming.
*
* @param <T> the item type
* @param joiner the function used to join the prefix and name
* @param pairs callback to add all the pairs
* @return a BiConsumer for use with the JsonWriter
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> flat(
Joiner joiner,
Consumer<Pairs<T>> pairs);
/**
* Add pairs using nested naming (for example as used in ECS).
*
* @param <T> the item type
* @param pairs callback to add all the pairs
* @return a BiConsumer for use with the JsonWriter
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> nested(
Consumer<Pairs<T>> pairs);
/**
* Joins a prefix and a name.
*/
@FunctionalInterface
public interface Joiner {
/**
* Joins the given prefix and name.
*
* @param prefix the prefix
* @param name the name
* @return the joined result or null
*/
String join(String prefix, String name);
}
/**
* Callback used to add pairs.
*
* @param <T> the item type
*/
public class Pairs<T> {
/**
* Add pairs from map entries.
*
* @param <V> the map value type
* @param extractor the extractor used to provide the map
*/
public <V> void addMapEntries(Function<T, Map<String, V>> extractor);
/**
* Add pairs from an iterable.
*
* @param elementsExtractor the extractor used to provide the iterable
* @param pairExtractor the extractor used to provide the name and value
* @param <E> the element type
*/
public <E> void add(
Function<T, Iterable<E>> elementsExtractor,
PairExtractor<E> pairExtractor);
/**
* Add pairs from an iterable.
*
* @param elementsExtractor the extractor used to provide the iterable
* @param nameExtractor the extractor used to provide the name
* @param valueExtractor the extractor used to provide the value
* @param <E> the element type
* @param <V> the value type
*/
public <E, V> void add(
Function<T, Iterable<E>> elementsExtractor,
Function<E, String> nameExtractor,
Function<E, V> valueExtractor);
/**
* Add pairs using the given callback.
*
* @param <V> the value type
* @param pairs callback provided with the item and consumer that can be called to
* actually add the pairs
*/
public <V> void add(BiConsumer<T, BiConsumer<String, V>> pairs);
}
}Structured Logging Usage Example:
// Custom structured log formatter
public class MyCustomFormatter implements StructuredLogFormatter<ILoggingEvent> {
private final JsonWriter<ILoggingEvent> jsonWriter;
public MyCustomFormatter(Environment environment,
StackTracePrinter stackTracePrinter) {
this.jsonWriter = JsonWriter.of((members) -> {
members.add("timestamp", ILoggingEvent::getTimeStamp);
members.add("level", (event) -> event.getLevel().toString());
members.add("logger", ILoggingEvent::getLoggerName);
members.add("message", ILoggingEvent::getFormattedMessage);
members.add("thread", ILoggingEvent::getThreadName);
members.add("application", environment.getProperty("spring.application.name"));
members.add("stackTrace").whenNotNull((event) -> {
IThrowableProxy throwable = event.getThrowableProxy();
return (throwable != null && stackTracePrinter != null)
? stackTracePrinter.print(throwable)
: null;
});
}).withNewLineAtEnd();
}
@Override
public String format(ILoggingEvent event) {
return this.jsonWriter.writeToString(event);
}
}Application Properties Configuration:
# Enable structured logging
logging.structured.format.console=ecs
logging.structured.format.file=logstash
# Configure JSON properties
logging.structured.json.add.spring-application-name=true
logging.structured.json.add.spring-application-group=true
logging.structured.json.include-mdc-keys=trueSpring Boot provides various properties to configure the logging system:
# Logging level configuration
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.springframework.web=WARN
# File logging
logging.file.name=/var/log/myapp.log
logging.file.path=/var/log
logging.file.max-size=10MB
logging.file.max-history=7
# Console logging
logging.console.enabled=true
logging.console.charset=UTF-8
# Pattern configuration
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.level=%5p
# Correlation ID
logging.expect-correlation-id=true
logging.pattern.correlation=[%correlationId]
# Log groups
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=TRACE# Structured logging format
logging.structured.format.console=ecs
logging.structured.format.file=gelf
# JSON member configuration
logging.structured.json.add.spring-application-name=true
logging.structured.json.add.spring-application-group=true
logging.structured.json.include-mdc-keys=true
logging.structured.json.exclude-mdc-keys=password,secret
# Context configuration
logging.structured.json.context.include=true
logging.structured.json.context.prefix=context_
# Stack trace configuration
logging.structured.json.stack-trace.include=always
logging.structured.json.stack-trace.max-depth=1000Here's a comprehensive example demonstrating various logging features:
package com.example.logging;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.logging.DeferredLogs;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ConfigurableApplicationContext;
import org.apache.commons.logging.Log;
import org.slf4j.MDC;
@SpringBootApplication
public class LoggingDemoApplication {
private static final DeferredLogs deferredLogs = new DeferredLogs();
private static final Log logger = deferredLogs.getLog(LoggingDemoApplication.class);
public static void main(String[] args) {
// Early logging (deferred)
logger.info("Starting application initialization...");
logger.debug("Loading configuration");
// Run the application
SpringApplication app = new SpringApplication(LoggingDemoApplication.class);
ConfigurableApplicationContext context = app.run(args);
// Switch deferred logs to actual logging system
deferredLogs.switchOverAll();
// Demonstrate logging system access
LoggingSystem loggingSystem = LoggingSystem.get(
LoggingDemoApplication.class.getClassLoader());
// Set log levels programmatically
loggingSystem.setLogLevel("com.example.logging.service", LogLevel.DEBUG);
loggingSystem.setLogLevel("com.example.logging.repository", LogLevel.TRACE);
// Use MDC for correlation
MDC.put("requestId", "req-12345");
MDC.put("userId", "user-67890");
logger.info("Processing request with context");
MDC.clear();
logger.info("Application started successfully");
}
}package com.example.logging.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public void processUser(String userId) {
// Add context to MDC
MDC.put("userId", userId);
try {
logger.debug("Starting user processing");
// Business logic
validateUser(userId);
enrichUserData(userId);
saveUser(userId);
logger.info("User processed successfully");
}
catch (Exception ex) {
logger.error("Failed to process user", ex);
throw ex;
}
finally {
MDC.remove("userId");
}
}
private void validateUser(String userId) {
logger.trace("Validating user: {}", userId);
// Validation logic
}
private void enrichUserData(String userId) {
logger.trace("Enriching user data: {}", userId);
// Enrichment logic
}
private void saveUser(String userId) {
logger.debug("Saving user: {}", userId);
// Save logic
}
}Configure JSON-formatted logs for centralized log aggregation systems (ELK, Splunk, CloudWatch).
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
// Request filter to add correlation IDs
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(CorrelationIdFilter.class);
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
private static final String CORRELATION_ID_MDC_KEY = "correlationId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Get or generate correlation ID
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
// Add to MDC for logging
MDC.put(CORRELATION_ID_MDC_KEY, correlationId);
MDC.put("requestUri", request.getRequestURI());
MDC.put("requestMethod", request.getMethod());
MDC.put("remoteAddr", request.getRemoteAddr());
// Add to response headers
response.setHeader(CORRELATION_ID_HEADER, correlationId);
try {
log.info("Incoming request");
filterChain.doFilter(request, response);
log.info("Request completed with status: {}", response.getStatus());
} finally {
MDC.clear();
}
}
}
// Service with structured logging
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order processOrder(String orderId, String customerId) {
// Add business context to MDC
MDC.put("orderId", orderId);
MDC.put("customerId", customerId);
try {
log.info("Processing order");
// Use structured key-value logging
log.atInfo()
.setMessage("Order validation started")
.addKeyValue("orderAmount", 1500.00)
.addKeyValue("orderStatus", "PENDING")
.log();
Order order = validateAndProcessOrder(orderId, customerId);
log.atInfo()
.setMessage("Order processed successfully")
.addKeyValue("orderAmount", order.getAmount())
.addKeyValue("orderStatus", order.getStatus())
.addKeyValue("processingTimeMs", order.getProcessingTime())
.log();
return order;
} catch (InsufficientFundsException e) {
log.atWarn()
.setMessage("Order declined due to insufficient funds")
.addKeyValue("requiredAmount", e.getRequiredAmount())
.addKeyValue("availableAmount", e.getAvailableAmount())
.setCause(e)
.log();
throw e;
} catch (Exception e) {
log.atError()
.setMessage("Order processing failed")
.setCause(e)
.log();
throw new OrderProcessingException("Failed to process order", e);
} finally {
MDC.remove("orderId");
MDC.remove("customerId");
}
}
private Order validateAndProcessOrder(String orderId, String customerId) {
// Implementation
return new Order();
}
}logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- JSON encoder for structured logging -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>true</includeContext>
<includeMdc>true</includeMdc>
<includeStructuredArguments>true</includeStructuredArguments>
<includeTags>true</includeTags>
<includeCallerData>false</includeCallerData>
<!-- Custom fields -->
<customFields>{"application":"my-app","environment":"production"}</customFields>
<!-- Field names customization -->
<fieldNames>
<timestamp>@timestamp</timestamp>
<message>message</message>
<logger>logger</logger>
<thread>thread</thread>
<level>level</level>
</fieldNames>
</encoder>
</appender>
<!-- JSON file appender with rolling -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/application.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/application-%d{yyyy-MM-dd}.json.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>true</includeContext>
<includeMdc>true</includeMdc>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
<appender-ref ref="JSON_FILE"/>
</root>
</configuration>application.yml:
logging:
level:
root: INFO
com.example: DEBUG
expect-correlation-id: trueUse Cases:
Enable runtime log level changes without application restart.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.logging.LoggersEndpoint;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin/logging")
public class LoggingController {
private static final Logger log = LoggerFactory.getLogger(LoggingController.class);
private final LoggersEndpoint loggersEndpoint;
private final LoggingSystem loggingSystem;
public LoggingController(LoggersEndpoint loggersEndpoint,
LoggingSystem loggingSystem) {
this.loggersEndpoint = loggersEndpoint;
this.loggingSystem = loggingSystem;
}
@GetMapping("/levels")
public Map<String, LogLevel> getAllLoggers() {
return loggersEndpoint.loggers().getLoggers().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().getConfiguredLevel()
));
}
@GetMapping("/levels/{name}")
public LoggersEndpoint.LoggerLevels getLoggerLevel(@PathVariable String name) {
return loggersEndpoint.loggerLevels(name);
}
@PostMapping("/levels/{name}")
public void setLoggerLevel(@PathVariable String name,
@RequestParam LogLevel level) {
log.info("Changing log level for {} to {}", name, level);
loggersEndpoint.configureLogLevel(name, level);
}
@PostMapping("/levels/group/{groupName}")
public void setLoggerGroupLevel(@PathVariable String groupName,
@RequestParam LogLevel level) {
log.info("Changing log level for group {} to {}", groupName, level);
loggingSystem.setLogLevel(groupName, level);
}
}
// Service for programmatic log level management
@Service
public class DynamicLoggingService {
private static final Logger log = LoggerFactory.getLogger(DynamicLoggingService.class);
private final LoggingSystem loggingSystem;
public DynamicLoggingService(LoggingSystem loggingSystem) {
this.loggingSystem = loggingSystem;
}
// Enable debug logging temporarily
public <T> T executeWithDebugLogging(String loggerName,
Supplier<T> operation) {
LoggerConfiguration originalConfig =
loggingSystem.getLoggerConfiguration(loggerName);
LogLevel originalLevel = originalConfig != null ?
originalConfig.getConfiguredLevel() : null;
try {
// Enable debug temporarily
loggingSystem.setLogLevel(loggerName, LogLevel.DEBUG);
log.info("Enabled DEBUG logging for {}", loggerName);
return operation.get();
} finally {
// Restore original level
if (originalLevel != null) {
loggingSystem.setLogLevel(loggerName, originalLevel);
log.info("Restored logging level for {} to {}", loggerName, originalLevel);
}
}
}
// Scheduled task to reset debug loggers
@Scheduled(fixedRate = 3600000) // Every hour
public void resetDebugLoggers() {
List<LoggerConfiguration> debugLoggers = loggingSystem.getLoggerConfigurations()
.stream()
.filter(config -> config.getConfiguredLevel() == LogLevel.DEBUG)
.toList();
if (!debugLoggers.isEmpty()) {
log.warn("Found {} loggers still at DEBUG level, resetting to INFO",
debugLoggers.size());
debugLoggers.forEach(config ->
loggingSystem.setLogLevel(config.getName(), LogLevel.INFO));
}
}
}application.yml:
management:
endpoints:
web:
exposure:
include: loggers
endpoint:
loggers:
enabled: true
logging:
group:
web: org.springframework.web,org.springframework.boot.web
database: org.hibernate,org.springframework.jdbc,org.jooq
security: org.springframework.securityUse Cases:
Configure asynchronous logging to minimize performance impact.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class HighThroughputService {
private static final Logger log = LoggerFactory.getLogger(HighThroughputService.class);
public CompletableFuture<Void> processHighVolume(List<Data> items) {
long startTime = System.currentTimeMillis();
// Async logging won't block processing
log.info("Starting high-volume processing of {} items", items.size());
return CompletableFuture.runAsync(() -> {
int processed = 0;
int errors = 0;
for (Data item : items) {
try {
processItem(item);
processed++;
// Log every 1000 items without blocking
if (processed % 1000 == 0) {
log.debug("Processed {} items", processed);
}
} catch (Exception e) {
errors++;
log.error("Failed to process item: {}", item.getId(), e);
}
}
long duration = System.currentTimeMillis() - startTime;
log.info("Completed processing: {} successful, {} errors, {}ms duration",
processed, errors, duration);
});
}
private void processItem(Data item) {
// Processing logic
}
}logback-spring.xml with async appender:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Regular console appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- File appender for async wrapping -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Async wrapper for file appender -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<!-- Queue size -->
<queueSize>512</queueSize>
<!-- Block when queue is full (false = drop messages) -->
<neverBlock>false</neverBlock>
<!-- Max flush time on shutdown -->
<maxFlushTime>5000</maxFlushTime>
<!-- Include caller data (expensive, disable for performance) -->
<includeCallerData>false</includeCallerData>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>Use Cases:
Manage different logging strategies across environments using Spring profiles.
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class LoggingConfiguration {
// Development logging configuration
@Configuration
@Profile("development")
static class DevelopmentLoggingConfig {
@Bean
public LoggingCustomizer developmentLoggingCustomizer(LoggingSystem loggingSystem) {
return () -> {
// Verbose logging for development
loggingSystem.setLogLevel("com.example", LogLevel.DEBUG);
loggingSystem.setLogLevel("org.springframework.web", LogLevel.DEBUG);
loggingSystem.setLogLevel("org.hibernate.SQL", LogLevel.DEBUG);
loggingSystem.setLogLevel("org.hibernate.type.descriptor.sql.BasicBinder", LogLevel.TRACE);
};
}
}
// Production logging configuration
@Configuration
@Profile("production")
static class ProductionLoggingConfig {
@Bean
public LoggingCustomizer productionLoggingCustomizer(LoggingSystem loggingSystem) {
return () -> {
// Minimal logging for production
loggingSystem.setLogLevel("ROOT", LogLevel.WARN);
loggingSystem.setLogLevel("com.example", LogLevel.INFO);
loggingSystem.setLogLevel("org.springframework", LogLevel.WARN);
loggingSystem.setLogLevel("org.hibernate", LogLevel.WARN);
};
}
}
// Testing logging configuration
@Configuration
@Profile("test")
static class TestLoggingConfig {
@Bean
public LoggingCustomizer testLoggingCustomizer(LoggingSystem loggingSystem) {
return () -> {
// Suppress most logs during tests
loggingSystem.setLogLevel("ROOT", LogLevel.ERROR);
loggingSystem.setLogLevel("com.example", LogLevel.INFO);
};
}
}
@FunctionalInterface
interface LoggingCustomizer {
void customize();
}
}application-development.yml:
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
pattern:
console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
file:
name: logs/dev-application.logapplication-production.yml:
logging:
level:
root: WARN
com.example: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} [%t] %-40.40logger{39} : %m%n"
file:
name: /var/log/myapp/application.log
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 30
total-size-cap: 10GBUse Cases:
Create custom appenders to route business-critical events to specialized systems.
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.stereotype.Service;
// Custom appender for business events
public class BusinessEventAppender extends AppenderBase<ILoggingEvent> {
private BusinessEventPublisher eventPublisher;
public void setEventPublisher(BusinessEventPublisher publisher) {
this.eventPublisher = publisher;
}
@Override
protected void append(ILoggingEvent event) {
// Only process events with BUSINESS_EVENT marker
if (event.getMarker() != null &&
event.getMarker().contains("BUSINESS_EVENT")) {
BusinessEvent businessEvent = new BusinessEvent(
event.getTimeStamp(),
event.getLevel().toString(),
event.getLoggerName(),
event.getFormattedMessage(),
event.getMDCPropertyMap()
);
if (eventPublisher != null) {
eventPublisher.publish(businessEvent);
}
}
}
}
// Business event publisher interface
public interface BusinessEventPublisher {
void publish(BusinessEvent event);
}
// Implementation that sends to message queue
@Service
public class KafkaBusinessEventPublisher implements BusinessEventPublisher {
private final KafkaTemplate<String, BusinessEvent> kafkaTemplate;
public KafkaBusinessEventPublisher(KafkaTemplate<String, BusinessEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void publish(BusinessEvent event) {
kafkaTemplate.send("business-events", event.getEventId(), event);
}
}
// Service using business event logging
@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
private static final Marker BUSINESS_EVENT = MarkerFactory.getMarker("BUSINESS_EVENT");
public void processPayment(Payment payment) {
MDC.put("paymentId", payment.getId());
MDC.put("amount", String.valueOf(payment.getAmount()));
MDC.put("currency", payment.getCurrency());
try {
// Regular logging
log.info("Processing payment");
executePayment(payment);
// Business event logging - routed to Kafka
log.info(BUSINESS_EVENT, "Payment processed successfully");
} catch (PaymentDeclinedException e) {
// Business event for declined payment
MDC.put("declineReason", e.getReason());
log.warn(BUSINESS_EVENT, "Payment declined", e);
throw e;
} finally {
MDC.clear();
}
}
private void executePayment(Payment payment) {
// Implementation
}
}logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Standard appenders -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Custom business event appender -->
<appender name="BUSINESS_EVENTS" class="com.example.logging.BusinessEventAppender">
<eventPublisher class="com.example.service.KafkaBusinessEventPublisher"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="BUSINESS_EVENTS"/>
</root>
</configuration>Use Cases:
log.info("User {}", userId) instead of string concatenationif (log.isDebugEnabled()) before expensive log message constructionincludeCallerData in async appenders for better performance@Service
public class RobustLoggingService {
private static final Logger log = LoggerFactory.getLogger(RobustLoggingService.class);
public void processWithLogging(String id) {
MDC.put("processId", id);
try {
log.info("Starting process");
performOperation(id);
log.info("Process completed successfully");
} catch (BusinessException e) {
// Log business exceptions at WARN level with context
log.warn("Business rule violation: {}", e.getMessage());
throw e;
} catch (Exception e) {
// Log technical exceptions at ERROR level with full stack trace
log.error("Technical error during process", e);
throw new ProcessingException("Failed to process " + id, e);
} finally {
// Always clean up MDC
MDC.remove("processId");
}
}
private void performOperation(String id) {
// Implementation
}
}import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(OutputCaptureExtension.class)
class LoggingTest {
@Test
void shouldLogExpectedMessage(CapturedOutput output) {
Logger log = LoggerFactory.getLogger(LoggingTest.class);
log.info("Test message with value: {}", 42);
assertThat(output).contains("Test message with value: 42");
}
}toString() of large objectsThis comprehensive documentation covers all major aspects of the Spring Boot logging system, providing developers with the knowledge needed to effectively configure and use logging in their applications.