Spring Boot Logging Complete Guide: From Basics to Production-Level Logging
Imagine you are working on a payment gateway that processes thousands of transactions every minute.
A customer reports that ₹5,000 was deducted from their wallet, but the merchant never received the payment confirmation.
You open the application, check the database, inspect the payment service, and search through multiple microservice logs. But there is one problem: you cannot easily identify which logs belong to that particular transaction.
Now think about another situation. Your application is running perfectly in development, but after deployment, a payment API starts returning HTTP 500. You have no idea which method failed, what the request was doing, or where the exception originated.
This is where proper application logging becomes extremely important.
Logging is not simply about printing messages in the console. It is one of the most important observability practices in production-grade Java applications.
In this tutorial, we will build a practical understanding of Spring Boot logging using a real-world payment processing example. We will start with the fundamentals and gradually move towards the techniques used by experienced backend developers.
1. What Is Logging in Java?
Logging is the process of recording application events, execution details, warnings, and errors while an application is running.
It helps developers understand what happened inside an application without attaching a debugger to a production server.
Consider a simple payment operation:
public void processPayment() {
System.out.println("Payment processing started");
// Payment business logic
System.out.println("Payment processing completed");
}
This approach works for a small demonstration. But it is not suitable for production applications.
Why?
- You cannot easily control different log levels.
- You cannot configure proper log formatting.
- You cannot easily manage log files and rotation.
- You cannot integrate a consistent logging framework.
- You have limited control over filtering and monitoring.
Professional Java applications use logging frameworks to solve these problems.
2. Understanding SLF4J and Logback
One common interview question is: What is the difference between SLF4J and Logback?
Let's understand them separately.
SLF4J
SLF4J stands for Simple Logging Facade for Java.
It provides a common logging API that allows your application code to communicate with a logging implementation without being tightly coupled to it.
For example:
private static final Logger log =
LoggerFactory.getLogger(PaymentService.class);
log.info("Payment processing started");
Your application uses the SLF4J API, while the underlying logging provider handles the actual logging operations.
Logback
Logback is a logging implementation that works with SLF4J.
It is the default logging implementation in a standard Spring Boot starter configuration.
Logback provides:
- Console logging.
- File-based logging.
- Rolling log files.
- Log level configuration.
- Custom log patterns.
- Logger configuration.
How do they work together?
Important: SLF4J is the logging abstraction, whereas Logback is one possible logging implementation.
3. Add Logging Dependencies in Spring Boot
If you are using Spring Boot with the standard starter, logging support
is already included through spring-boot-starter-logging,
which is brought in by spring-boot-starter-web.
Maven dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Gradle dependency
implementation 'org.springframework.boot:spring-boot-starter-web'
You generally do not need to add SLF4J or Logback separately when using the standard Spring Boot starter.
To verify your logging dependencies in Maven:
mvn dependency:tree
For Gradle:
./gradlew dependencies --configuration runtimeClasspath
4. Understanding Different Logging Levels
Logging levels help us decide which messages should be recorded and how important they are.
Spring Boot commonly uses these logging levels:
| Level | Purpose | Real-world example |
|---|---|---|
| TRACE | Very detailed execution information. | Tracing internal calculation steps. |
| DEBUG | Detailed diagnostic information. | Checking payment validation decisions. |
| INFO | Important application events. | Payment successfully processed. |
| WARN | Unexpected situations that do not necessarily stop execution. | External vendor response is slow. |
| ERROR | Failures requiring investigation. | Payment processing failed. |
| OFF | Disables logging for a logger. | Disabling a specific logger. |
Example: Choosing the right logging level
log.trace("Entering payment calculation");
log.debug("Payment amount: {}", amount);
log.info("Payment request received");
log.warn("Payment vendor response is delayed");
log.error("Payment processing failed", exception);
Senior developer tip: Do not log everything as INFO or ERROR. If every normal event is an ERROR, your production monitoring becomes noisy and important failures become harder to identify.
5. Create a Real-World Spring Boot Payment Logging Project
Let's build a simple payment processing flow.
Our application will contain:
- PaymentController: Receives the payment request.
- PaymentService: Executes the business logic.
- PaymentRepository: Represents the persistence layer.
- SLF4J Logger: Records application events.
Our expected flow is:
Step 1: Create PaymentRequest DTO
public class PaymentRequest {
private String transactionId;
private BigDecimal amount;
private String currency;
// Getters and setters
}
Step 2: Create PaymentController
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private static final Logger log =
LoggerFactory.getLogger(PaymentController.class);
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping
public ResponseEntity<String> processPayment(
@RequestBody PaymentRequest request) {
log.info("Payment API request received. TransactionId={}",
request.getTransactionId());
paymentService.processPayment(request);
log.info("Payment API completed. TransactionId={}",
request.getTransactionId());
return ResponseEntity.ok("Payment processed successfully");
}
}
Notice that we are using parameterized logging with placeholders instead of string concatenation.
Prefer this:
log.info("Payment amount: {}", amount);
Instead of:
log.info("Payment amount: " + amount);
Parameterized logging avoids unnecessary string construction when the corresponding log level is disabled.
Step 3: Create PaymentService
@Service
public class PaymentService {
private static final Logger log =
LoggerFactory.getLogger(PaymentService.class);
public void processPayment(PaymentRequest request) {
log.info("Starting payment processing. TransactionId={}",
request.getTransactionId());
try {
validatePayment(request);
// Call payment vendor here.
log.info("Payment successfully processed. TransactionId={}",
request.getTransactionId());
} catch (IllegalArgumentException ex) {
log.warn("Payment validation failed. TransactionId={}",
request.getTransactionId());
throw ex;
} catch (Exception ex) {
log.error("Unexpected payment processing failure. TransactionId={}",
request.getTransactionId(), ex);
throw ex;
}
}
private void validatePayment(PaymentRequest request) {
if (request.getAmount() == null ||
request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(
"Payment amount must be greater than zero");
}
}
}
In a real application, you should also use a global exception handler and define clear exception types for validation, vendor failures, and unexpected system errors.
Step 4: What does the output look like?
2026-11-08 10:30:10 INFO PaymentController
Payment API request received. TransactionId=TXN10001
2026-11-08 10:30:10 INFO PaymentService
Starting payment processing. TransactionId=TXN10001
2026-11-08 10:30:11 INFO PaymentService
Payment successfully processed. TransactionId=TXN10001
2026-11-08 10:30:11 INFO PaymentController
Payment API completed. TransactionId=TXN10001
Now we can understand the execution flow by reading the logs.
6. Configure Logging Using application.properties
Spring Boot allows us to configure logging without writing Java code.
Open your application.properties file.
# Root logging level
logging.level.root=INFO
# Application package logging
logging.level.com.example.payment=DEBUG
# Spring framework logging
logging.level.org.springframework=WARN
# Hibernate logging
logging.level.org.hibernate=ERROR
# Console log pattern
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
# Log file
logging.file.name=logs/payment-service.log
Let's understand the important properties.
| Property | Purpose |
|---|---|
logging.level.root |
Sets the default logging level. |
logging.level.package |
Sets logging level for a particular package. |
logging.file.name |
Writes logs to a specified file. |
logging.pattern.console |
Customizes console output. |
logging.pattern.file |
Customizes file output. |
7. Different Logging Levels for Different Environments
A common mistake is using DEBUG logging everywhere, including production.
Development and production environments have different requirements.
application-dev.properties
logging.level.root=DEBUG
logging.level.com.example.payment=DEBUG
application-prod.properties
logging.level.root=INFO
logging.level.com.example.payment=INFO
logging.level.org.springframework=WARN
Activate the production profile using:
spring.profiles.active=prod
Or set the environment variable:
SPRING_PROFILES_ACTIVE=prod
This keeps unnecessary diagnostic logs out of the production environment.
8. Configure Logback Using logback-spring.xml
When an application becomes larger, application.properties
may not provide enough control over logging configuration.
For more advanced requirements, we can use logback-spring.xml.
Create this file inside:
src/main/resources/logback-spring.xml
Example production-oriented configuration:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_DIR" value="logs"/>
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
%-5level [%thread] %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/payment-service.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_DIR}/archive/payment-%d{yyyy-MM-dd}.%i.log.gz
</fileNamePattern>
<maxFileSize>20MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
%-5level [%thread] %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
This configuration provides:
- Console logging.
- File-based logging.
- Daily log file rollover.
- File rollover when the configured size is reached.
- Compressed archived logs.
- Maximum history and total archive size limits.
Production note: Log rotation does not automatically make logs durable or searchable. In containerized environments, consider writing structured logs to stdout and letting the infrastructure collect, retain, and forward them to a centralized logging platform.
9. MDC: The Most Important Logging Concept for Microservices
Now let's discuss something that separates basic logging from production-level logging.
Imagine your payment gateway contains these microservices:
- API Gateway
- Wallet Service
- Payment Service
- Notification Service
- Vendor Integration Service
A single transaction may travel through all five services.
Without a common identifier, identifying all log entries belonging to the same request can be difficult.
This is where MDC comes into the picture.
What is MDC?
MDC stands for Mapped Diagnostic Context.
It allows us to store contextual information, such as transaction IDs, request IDs, and user identifiers, so that the logging framework can automatically include them in log messages.
Step 1: Create a correlation ID filter
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String HEADER_NAME = "X-Correlation-ID";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String correlationId = request.getHeader(HEADER_NAME);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
try {
MDC.put("correlationId", correlationId);
response.setHeader(HEADER_NAME, correlationId);
filterChain.doFilter(request, response);
} finally {
MDC.remove("correlationId");
}
}
}
We generate a correlation ID if the incoming request does not contain one.
The finally block is important because it prevents stale
MDC data from leaking into another request when threads are reused.
Step 2: Update the logging pattern
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [correlationId=%X{correlationId}] %logger{36} - %msg%n
Step 3: Write normal logs
log.info("Payment request received");
log.info("Wallet balance validated");
log.info("Vendor payment response received");
Now the output can look like this:
INFO [correlationId=TXN-10001]
Payment request received
INFO [correlationId=TXN-10001]
Wallet balance validated
INFO [correlationId=TXN-10001]
Vendor payment response received
All these logs can be searched using the same correlation ID.
Important senior-level consideration: MDC is normally
thread-local. When work moves to another thread through
@Async, executors, or asynchronous processing, the context
does not automatically follow in every setup. You must explicitly
propagate it using an appropriate TaskDecorator,
context-propagation mechanism, or tracing library.
For Kafka and other messaging systems, propagate the correlation ID through message headers and establish MDC context in the consumer before processing.
10. How to Log Exceptions Correctly
One common mistake made by Java developers is logging only the exception message.
Consider:
try {
processPayment();
} catch (Exception ex) {
log.error(ex.getMessage());
}
This is not sufficient for debugging production failures because it does not include the complete stack trace.
Prefer:
try {
processPayment();
} catch (Exception ex) {
log.error("Payment processing failed", ex);
}
Passing the exception object allows the logging framework to record the stack trace.
Also avoid logging the same exception at every layer. Usually, handle it at the appropriate boundary, preserving the original cause when rethrowing or wrapping exceptions.
11. Logging Sensitive Information: A Critical Security Practice
Payment applications process sensitive information. Logging complete request and response objects without checking their contents can create serious security problems.
Never log:
- Passwords.
- OTP values.
- PINs.
- Authentication tokens.
- Full card numbers or CVV.
- Bank account credentials.
- Encryption keys.
- Unmasked personal information.
Bad logging practice
log.info("Payment request: {}", request);
This might expose sensitive information if the DTO contains credentials or personal data.
Better logging practice
log.info(
"Payment request received. transactionId={}, amount={}, currency={}",
request.getTransactionId(),
request.getAmount(),
request.getCurrency()
);
Log only the fields required for troubleshooting, subject to your organization's security and data-retention policies.
Example: Masking a phone number
public static String maskPhoneNumber(String phone) {
if (phone == null || phone.length() < 4) {
return "****";
}
return "******" + phone.substring(phone.length() - 4);
}
Output:
Original: 4234567890
Masked: ******7890
Masking should be applied before information enters the logging pipeline. It is not a substitute for avoiding sensitive data in logs in the first place.
12. JSON Logging for Microservices
In modern distributed systems, plain text logs can become difficult to search and analyze.
Structured logging solves this problem by recording events as JSON fields.
For example:
{
"timestamp": "2026-11-08T10:30:10.120Z",
"level": "INFO",
"service": "payment-service",
"correlationId": "TXN-10001",
"transactionId": "TXN-10001",
"message": "Payment processed successfully"
}
These fields can be searched independently in centralized log platforms.
Common tools used for log collection and analysis include:
- Elasticsearch
- Logstash
- Kibana
- Grafana Loki
- Fluent Bit
- Splunk
For Spring Boot applications, JSON logging can be configured using an appropriate Logback JSON encoder or a structured logging solution compatible with your Spring Boot version.
Do not assume that adding a JSON logging dependency automatically produces safe or useful logs. Define the fields, masking rules, retention policy, and ingestion pipeline deliberately.
13. Dynamic Logging Level Using Spring Boot Actuator
Sometimes production debugging requires temporarily enabling DEBUG logs for one package without restarting the application.
Spring Boot Actuator provides a loggers endpoint for this purpose.
Add Actuator dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Configure application.properties
management.endpoints.web.exposure.include=health,info,loggers
To inspect a logger:
GET /actuator/loggers/com.example.payment
To change its logging level:
POST /actuator/loggers/com.example.payment
Content-Type: application/json
{
"configuredLevel": "DEBUG"
}
To restore the inherited logging level:
POST /actuator/loggers/com.example.payment
Content-Type: application/json
{
"configuredLevel": null
}
Security warning: Never expose management endpoints publicly without proper authentication and authorization. Restrict access to trusted operators and internal networks.
14. Common Logging Mistakes in Spring Boot
| Mistake | Better approach |
|---|---|
Using System.out.println() |
Use SLF4J. |
| Logging every message at INFO | Choose meaningful log levels. |
| Logging complete request objects | Log only approved fields. |
| Ignoring stack traces | Pass the exception object. |
| Using DEBUG in production everywhere | Enable targeted diagnostic logging. |
| No correlation ID | Use request IDs and distributed tracing. |
| No log rotation | Configure retention and archival. |
| Logging the same exception repeatedly | Log at the appropriate handling boundary. |
15. Logging vs Monitoring vs Distributed Tracing
These three concepts are related, but they solve different problems.
| Concept | Purpose | Example |
|---|---|---|
| Logging | Records individual application events. | Payment failed due to vendor timeout. |
| Monitoring | Tracks system health and metrics. | Payment API error rate is 5%. |
| Distributed tracing | Tracks requests across multiple services. | Gateway → Wallet → Payment → Vendor. |
For a mature microservices architecture, these capabilities should work together.
OpenTelemetry is a widely used option for collecting traces, metrics, and related telemetry across distributed systems.
16. Important Spring Boot Logging Interview Questions and Answers
Q1. What is the difference between SLF4J and Logback?
SLF4J is a logging abstraction that provides a common API. Logback is a logging implementation that processes and writes the log messages.
Q2. What is the default logging framework in Spring Boot?
Logback is the default logging implementation when using Spring Boot's standard logging starter configuration.
Q3. Why should we use SLF4J instead of System.out.println()?
SLF4J supports configurable log levels, structured logging integrations, multiple logging providers, and production-friendly logging practices.
Q4. What is MDC in Java logging?
Mapped Diagnostic Context stores contextual information such as correlation IDs in the logging context. Logback can include these values in log output automatically.
Q5. What is the difference between DEBUG and INFO?
DEBUG is intended for diagnostic details, while INFO records significant normal application events.
Q6. What is the difference between ERROR and WARN?
ERROR indicates a failure that requires attention. WARN indicates an unexpected condition that may require investigation but does not necessarily mean the operation has failed.
Q7. How do you configure logging in Spring Boot?
Logging can be configured using application.properties, application.yml, or a Logback configuration file such as logback-spring.xml.
Q8. What is the purpose of logback-spring.xml?
It provides advanced Logback configuration, including appenders, patterns, rolling policies, filters, and profile-specific logging configuration.
Q9. What is a RollingFileAppender?
It writes logs to files and rolls over to new files based on configured time or size policies, helping control disk usage.
Q10. Why should we avoid logging passwords and OTPs?
Logs may be accessed by multiple systems and retained for long periods. Sensitive information in logs can cause security and compliance issues.
Q11. How do you trace a request across multiple microservices?
Use correlation IDs or distributed tracing context. Propagate the identifiers through HTTP headers and messaging headers, and include them in application logs.
Q12. Does MDC automatically propagate to another thread?
No. Standard MDC implementations are generally thread-local. For asynchronous processing, context propagation must be configured explicitly.
Q13. How can you change logging levels without restarting Spring Boot?
Spring Boot Actuator provides a loggers endpoint that can change the configured logging level dynamically, provided the endpoint is enabled and secured.
Q14. What is the difference between logging and distributed tracing?
Logging records individual events, whereas distributed tracing follows a request across services and records the timing and relationships between operations.
Q15. How would you design logging for a high-volume payment gateway?
I would use asynchronous or appropriately managed logging where suitable, structured JSON output, correlation IDs, controlled log levels, sensitive-data masking, centralized log aggregation, retention policies, and monitoring. I would also avoid excessive logging in high-throughput paths and measure the performance impact.
Q16. What happens if logging itself fails?
Logging failures should not normally bring down the payment business operation. I would configure appropriate appenders, monitor logging infrastructure, manage disk usage, and ensure the application has a reliable telemetry delivery strategy.
Q17. Why should we avoid duplicate exception logging?
Logging an exception at every layer creates noisy logs, duplicates stack traces, and makes production troubleshooting difficult. Log it at the appropriate handling boundary and preserve its cause.
Q18. Can we use Log4j2 instead of Logback in Spring Boot?
Yes. Spring Boot supports alternative logging implementations. We can exclude the default logging starter and configure the appropriate Log4j2 starter, ensuring that only the intended logging provider is active.
Q19. How do you investigate a slow payment API using logs?
I would search using the correlation ID, inspect the timestamps of each operation, identify the slow dependency, compare application and vendor response times, and correlate the logs with metrics and distributed traces.
Q20. Should we log every database query in production?
No. Logging every query can generate enormous volumes of data, expose sensitive values, and affect performance. I would enable targeted database diagnostics when needed and use appropriate database monitoring tools.
17. Senior Java Developer Production Logging Checklist
Before deploying a Spring Boot microservice, I would review the following points:
- Use SLF4J with a properly configured logging implementation.
- Configure appropriate log levels for each environment.
- Use correlation IDs for request tracking.
- Propagate diagnostic context across asynchronous operations.
- Mask sensitive information before logging.
- Include complete exception details when appropriate.
- Configure log rotation and retention.
- Prefer structured logs for centralized analysis.
- Protect Actuator management endpoints.
- Monitor logging volume, disk usage, and ingestion failures.
- Integrate logging with monitoring and distributed tracing.
- Avoid unnecessary logging inside high-throughput loops.
18. Final Thoughts
Logging is one of those engineering practices that developers often underestimate until something goes wrong in production.
Writing a few log statements is easy. Designing a logging strategy that helps a team investigate a failed payment across multiple microservices is a completely different challenge.
As a Java backend developer, you should understand not only how to write logs but also how to make them useful, searchable, secure, and reliable.
My recommendation is to start with SLF4J and Logback, then learn MDC, correlation IDs, structured logging, and distributed tracing. These concepts will make a significant difference when you start working on enterprise-level Spring Boot applications.
Remember: Good logging does not mean generating more logs. It means generating the right logs at the right time with the right context.
Test Your Spring Boot Logging Knowledge
Let's finish with one practical interview question.
Question: Your Spring Boot application processes 10,000 requests per minute. You are using MDC to store correlation IDs. What is the biggest concern when the application processes requests using a reusable thread pool?
- MDC automatically generates duplicate IDs.
- MDC values may leak between requests if they are not cleared.
- SLF4J cannot work with multiple threads.
- Logback disables all INFO messages.
Correct Answer: B
Explanation: MDC is generally thread-local. Since thread pools reuse threads, failing to clear MDC values in a finally block can cause diagnostic context from one request to appear in another request's logs.
No comments:
Post a Comment