Production-Grade Logging in Spring Boot: A Complete Guide to Logging Levels, Files, JSON Logs, Correlation IDs, and Best Practices
Logging is one of the most important parts of a production backend system.
When everything works, you may not think much about logs.
But when production starts returning 500 errors at 2 AM, a customer reports that an API is failing, or a payment request behaves unexpectedly, logs become one of your most important debugging tools.
Poor logging makes production debugging painful.
Good logging helps you answer:
- What happened?
- When did it happen?
- Which user triggered it?
- Which request caused it?
- Which service handled it?
- How long did it take?
- What failed?
- Why did it fail?
- What should we investigate next?
In this article, we will build a production-grade logging strategy for a Java Spring Boot application.
1. What Does Production-Grade Logging Mean?
Production-grade logging is not simply:
log.info("User created");
Production logging should be:
- Structured
- Searchable
- Consistent
- Secure
- Configurable
- Environment-aware
- Correlated across requests
- Useful during debugging
- Suitable for monitoring and alerting
A good logging architecture might look like this:
Spring Boot Application
|
v
Logback
|
+---- application.log
|
+---- error.log
|
+---- audit.log
|
+---- access.log
|
v
Log Aggregation
|
+---- ELK
+---- Grafana Loki
+---- CloudWatch
+---- Datadog
+---- Splunk
The goal is not to log everything.
The goal is to log the right information at the right level.
2. Understanding Log Levels
Spring Boot uses SLF4J as the logging abstraction and commonly uses Logback as the underlying logging implementation.
The most common log levels are:
TRACE
DEBUG
INFO
WARN
ERROR
The order represents increasing severity.
TRACE
TRACE is the most detailed logging level.
Example:
log.trace("Entering calculateInvoice() with customerId={}", customerId);
Use TRACE for very detailed diagnostic information.
Usually:
Production: OFF
Development: Sometimes ON
Debugging: Useful
Avoid keeping TRACE enabled globally in production because it can generate huge amounts of logs.
3. DEBUG
DEBUG is useful for developers.
Example:
log.debug("Fetching customer with customerId={}", customerId);
Another example:
log.debug("Payment request received for orderId={}", orderId);
DEBUG logs are useful when troubleshooting a specific feature.
A common production strategy is:
INFO -> Default
DEBUG -> Temporarily enabled when troubleshooting
4. INFO
INFO should contain important application events.
For example:
log.info("User successfully created. userId={}", userId);
Or:
log.info("Order successfully created. orderId={}, customerId={}",
orderId,
customerId);
Good INFO logs might include:
Application started
User registered
Order created
Payment completed
File uploaded
Scheduled job completed
External integration connected
But don't log every line of your application at INFO.
5. WARN
WARN indicates something unexpected or potentially problematic.
Example:
log.warn("Login attempt failed. email={}", email);
Another example:
log.warn("Payment provider response time is high. durationMs={}",
durationMs);
WARN means:
"The application is still functioning, but someone should pay attention."
Examples:
- Retry occurred
- External API is slow
- Deprecated API was called
- Configuration is missing but has a fallback
- Login failed repeatedly
- Database connection pool is close to its limit
6. ERROR
ERROR represents a failure that needs investigation.
Example:
log.error("Failed to create order. orderId={}", orderId, exception);
Notice that the exception is passed separately:
log.error("Failed to create order", exception);
Instead of:
log.error("Failed to create order " + exception.getMessage());
The first approach preserves the stack trace.
7. Never Log Sensitive Information
One of the most important production logging rules is:
Logs should never become a source of sensitive data leakage.
Never log:
Passwords
Access tokens
Refresh tokens
API keys
Credit card numbers
CVV
Session IDs
Private keys
Authorization headers
Bad:
log.info("Login request: username={}, password={}",
username,
password);
Good:
log.info("Login attempt received. username={}", username);
Even better, depending on your privacy requirements, avoid logging email addresses or other personal identifiers unless there is a clear operational reason.
8. Use Parameterized Logging
Avoid string concatenation.
Don't do this:
log.info("User created: " + userId);
Prefer:
log.info("User created. userId={}", userId);
For multiple values:
log.info(
"Order created. orderId={}, customerId={}, amount={}",
orderId,
customerId,
amount
);
Parameterized logging is cleaner and avoids unnecessary string construction.
9. Create a Centralized Logging Configuration
Spring Boot makes it easy to configure Logback.
You can create:
src/main/resources/logback-spring.xml
A basic 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}
[%thread]
%-5level
%logger{36}
-
%msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Now the application produces readable logs such as:
2026-08-08 10:30:25.123 [http-nio-8080-exec-1] INFO
c.example.user.UserService -
User created. userId=123
10. Different Log Files for Different Levels
For a production application, you may want separate files.
For example:
logs/
├── application.log
├── error.log
├── audit.log
└── access.log
This makes troubleshooting easier.
For example:
application.log
contains general application events.
error.log
contains ERROR events.
audit.log
contains important security/business events.
access.log
contains HTTP request information.
11. Creating an ERROR Log File
Example:
<appender name="ERROR_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/error.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_DIR}/archive/error.%d{yyyy-MM-dd}.%i.log.gz
</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
[%thread]
%-5level
%logger{36}
-
%msg%n
</pattern>
</encoder>
</appender>
Now ERROR logs can be stored separately.
12. Why Log Rotation Matters
Imagine your application generates:
application.log
and you never rotate it.
After several months:
application.log = 150 GB
Your server's disk eventually becomes full.
This can cause much bigger problems.
For example:
Application
|
v
Disk full
|
+---- Logging fails
+---- Database operations may fail
+---- Temporary files cannot be created
+---- Application becomes unstable
That's why production applications need:
- Maximum file size
- Maximum history
- Total storage limit
- Compression
- Time-based rotation
Example:
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
13. Application Logs vs Audit Logs
Not every important event is an application error.
Consider:
Admin disabled user 123
This isn't an ERROR.
It is an audit event.
Create a separate audit logger.
private static final Logger auditLogger =
LoggerFactory.getLogger("AUDIT");
Then:
auditLogger.info(
"User disabled. adminId={}, targetUserId={}",
adminId,
targetUserId
);
This gives you a separate audit stream.
14. Audit Logging Is Extremely Important
For systems involving multiple users, administrators, payments, permissions, or sensitive operations, audit logging becomes extremely valuable.
Examples:
USER_CREATED
USER_DISABLED
USER_ENABLED
PASSWORD_CHANGED
ROLE_CHANGED
LOGIN_SUCCESS
LOGIN_FAILED
API_KEY_CREATED
API_KEY_REVOKED
DATA_EXPORTED
PAYMENT_COMPLETED
Instead of:
log.info("Something happened");
Use structured information:
auditLogger.info(
"AUDIT event=ROLE_CHANGED actorId={} targetUserId={} oldRole={} newRole={}",
actorId,
targetUserId,
oldRole,
newRole
);
Now the event can easily be searched.
15. Logging HTTP Requests
For backend systems, request logging is extremely useful.
You want to know:
HTTP Method
URL
Status Code
Execution Time
Request ID
User ID
Example:
GET /api/users/123
status=200
duration=45ms
requestId=9f7a2
A servlet filter is one approach.
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log =
LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
long start = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration =
System.currentTimeMillis() - start;
log.info(
"HTTP request method={} uri={} status={} durationMs={}",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration
);
}
}
}
This gives you a basic access log.
16. Correlation IDs
This is one of the most useful concepts in distributed systems.
Imagine a request:
Frontend
|
v
API Gateway
|
v
User Service
|
v
Payment Service
|
v
Notification Service
One request could generate dozens of logs.
How do you identify which logs belong to the same request?
Use a:
Correlation ID
Example:
requestId=7f83ab29
Then every service logs:
requestId=7f83ab29
Now you can search the entire system using that ID.
17. Implementing Correlation ID with MDC
SLF4J provides MDC:
MDC.put("requestId", requestId);
Example:
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String REQUEST_ID = "requestId";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String requestId = request.getHeader(REQUEST_ID);
if (requestId == null || requestId.isBlank()) {
requestId = UUID.randomUUID().toString();
}
MDC.put(REQUEST_ID, requestId);
response.setHeader(REQUEST_ID, requestId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove(REQUEST_ID);
}
}
}
Now add it to Logback:
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
[%thread]
%-5level
[%X{requestId}]
%logger{36}
-
%msg%n
</pattern>
The resulting log becomes:
2026-08-08 12:20:10.120
[http-nio-8080-exec-2]
INFO
[7f83ab29]
UserService -
Fetching user userId=123
This is much easier to debug.
18. Structured Logging
Traditional logs look like:
User created successfully userId=123
Structured logs can look like JSON:
{
"timestamp": "2026-08-08T12:20:10.120Z",
"level": "INFO",
"service": "user-service",
"requestId": "7f83ab29",
"userId": "123",
"event": "USER_CREATED"
}
This is much easier for log aggregation systems to process.
For production environments, structured JSON logging is often preferable.
19. Why JSON Logs Are Better for Production
Suppose you use:
ELK
Grafana Loki
Datadog
AWS CloudWatch
Splunk
You can query structured fields.
For example:
level = ERROR
service = payment-service
environment = production
Or:
requestId = 7f83ab29
Or:
durationMs > 1000
This becomes much more powerful than searching plain text.
20. Logging Exceptions Correctly
Bad:
try {
paymentService.process(payment);
} catch (Exception e) {
log.error("Payment failed: " + e.getMessage());
}
This loses the stack trace.
Better:
try {
paymentService.process(payment);
} catch (Exception e) {
log.error(
"Payment processing failed. paymentId={}",
paymentId,
e
);
}
Now you get:
ERROR Payment processing failed. paymentId=123
java.lang.IllegalStateException: Payment provider timeout
at PaymentService.process(...)
at PaymentController.create(...)
The stack trace is extremely important for debugging.
21. Don't Log the Same Exception Multiple Times
A common mistake is:
Repository
↓
Service
↓
Controller
Every layer catches and logs the same exception.
You might get:
ERROR Database failure
ERROR Service failure
ERROR Controller failure
Three logs for one problem.
Prefer centralized exception handling when possible.
For Spring Boot:
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(
Exception exception) {
log.error(
"Unhandled application exception",
exception
);
return ResponseEntity
.internalServerError()
.body("Something went wrong");
}
}
Now unexpected exceptions can be logged centrally.
22. Logging Business Events
Not every useful log is technical.
Business events can be extremely valuable.
Example:
log.info(
"Order completed. orderId={}, customerId={}, amount={}, currency={}",
orderId,
customerId,
amount,
currency
);
This can help answer questions such as:
How many orders were completed?
Which payment failed?
How long does checkout take?
Which customer experienced the problem?
Logging should help both developers and operations teams.
23. Logging External API Calls
Suppose your application calls:
Stripe
Salesforce
OpenAI
AWS
Google Maps
Email provider
SMS provider
You should log useful metadata.
Example:
long start = System.currentTimeMillis();
try {
PaymentResponse response =
paymentClient.createPayment(request);
long duration =
System.currentTimeMillis() - start;
log.info(
"Payment provider call completed. provider={} status={} durationMs={}",
"stripe",
response.status(),
duration
);
} catch (Exception e) {
long duration =
System.currentTimeMillis() - start;
log.error(
"Payment provider call failed. provider={} durationMs={}",
"stripe",
duration,
e
);
}
But never log:
Authorization header
API key
Access token
Full card details
Sensitive request payload
24. Logging Database Operations
Don't log every SQL query in production unless you have a specific reason.
For example, enabling:
spring.jpa.show-sql=true
in production can create huge amounts of output.
For development:
spring.jpa.show-sql=true
may be useful.
For production:
spring.jpa.show-sql=false
Instead, monitor slow queries through proper database monitoring and profiling tools.
25. Different Logging Configuration Per Environment
Your logging configuration should change based on the environment.
Development:
DEBUG
Readable console logs
More diagnostic information
Production:
INFO
JSON logs
Error tracking
Structured fields
Log rotation
Centralized log collection
Example:
spring:
profiles:
active: dev
You can maintain:
application-dev.yml
application-prod.yml
And configure logging accordingly.
26. Production Logging Architecture
A practical production architecture might look like:
Spring Boot
|
v
Logback
|
+-------------+-------------+
| | |
v v v
Application Error Audit
Logs Logs Logs
| | |
+-------------+-------------+
|
v
Log Collector
|
+-------------+-------------+
| | |
v v v
CloudWatch Loki ELK
|
v
Dashboard
|
v
Alerts
This is much better than simply SSHing into a server and running:
tail -f application.log
every time something breaks.
27. Docker and Kubernetes Logging
If your application runs inside Docker or Kubernetes, writing logs only to local files may not be the best strategy.
A common approach is:
Application
|
v
stdout / stderr
|
v
Docker / Kubernetes
|
v
Log Collector
|
v
Centralized Logging Platform
For example:
Spring Boot
↓
stdout
↓
Docker
↓
Fluent Bit
↓
Elasticsearch
↓
Kibana
This allows logs to remain available even when containers are recreated.
28. Logging in Kubernetes
In Kubernetes, pods are disposable.
That means this:
Pod A
|
+--- application.log
is not necessarily a reliable long-term logging strategy.
Instead:
Pod
|
v
stdout
|
v
Container Runtime
|
v
Log Collector
|
v
Centralized Storage
This is generally more suitable for cloud-native applications.
29. Log Levels Should Be Intentional
A useful rule:
TRACE → Extremely detailed diagnostics
DEBUG → Developer troubleshooting
INFO → Important application events
WARN → Unexpected but recoverable situation
ERROR → Failure requiring investigation
Don't do this:
log.error("User logged in successfully");
Use:
log.info("User login successful. userId={}", userId);
Log levels should communicate severity.
30. Don't Log Everything
More logs do not automatically mean better observability.
Bad:
log.info("Starting method");
log.info("Entering service");
log.info("Repository called");
log.info("Repository returned");
log.info("Service completed");
log.info("Controller completed");
This creates noise.
Instead:
log.info(
"User profile updated. userId={} durationMs={}",
userId,
durationMs
);
Log meaningful events.
31. Logging Performance
Logging can affect application performance.
Especially dangerous:
log.debug(
"Huge object: {}",
objectWithThousandsOfFields
);
When DEBUG isn't enabled, parameterized logging helps avoid unnecessary string concatenation, but object serialization or expensive argument computation can still cost time.
Avoid:
log.debug("Response: {}", expensiveMethod());
if the computation itself is expensive.
You can guard expensive operations:
if (log.isDebugEnabled()) {
log.debug("Detailed response: {}", expensiveMethod());
}
Use this only when the computation is genuinely expensive.
32. Async Logging
High-throughput applications can benefit from asynchronous logging.
Instead of:
Application
|
v
Write log
|
v
Disk
you can use:
Application
|
v
Async Queue
|
v
Logger
|
v
Disk / Collector
This reduces the amount of time application threads spend waiting on logging operations.
However, asynchronous logging should be configured carefully to avoid losing logs during abrupt shutdowns and to prevent queue overflow.
33. Log Retention
Production logs should have a retention policy.
For example:
Application logs → 30 days
Audit logs → 90 days
Security logs → 180 days
The actual retention period should be based on:
- Compliance
- Security requirements
- Business requirements
- Storage cost
- Incident investigation needs
Don't keep everything forever.
34. A Practical Logback Configuration
A simplified production configuration could look like:
<configuration>
<property name="LOG_DIR" value="./logs"/>
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd'T'HH:mm:ss.SSS}
%-5level
[%X{requestId}]
%logger{36}
-
%msg%n
</pattern>
</encoder>
</appender>
<appender name="APPLICATION_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/application.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_DIR}/archive/application.%d{yyyy-MM-dd}.%i.log.gz
</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
%-5level
[%X{requestId}]
%logger{36}
-
%msg%n
</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>
${LOG_DIR}/archive/error.%d{yyyy-MM-dd}.%i.log.gz
</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}
%-5level
[%X{requestId}]
%logger{36}
-
%msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="APPLICATION_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
This gives you:
logs/
├── application.log
├── error.log
└── archive/
├── application.2026-08-08.0.log.gz
├── error.2026-08-08.0.log.gz
└── ...
35. Real-World Example
Imagine a customer reports:
"My payment failed."
Without structured logging, you might search:
payment failed
and find thousands of results.
With production-grade logging, you can search:
requestId=7f83ab29
Then you might see:
INFO requestId=7f83ab29
Payment request received. orderId=ORD-123
INFO requestId=7f83ab29
Calling payment provider. provider=stripe
WARN requestId=7f83ab29
Payment provider response slow. durationMs=4200
ERROR requestId=7f83ab29
Payment provider call failed. orderId=ORD-123
Now you know exactly what happened.
That's the real value of production logging.
36. Logging Best Practices Checklist
Before deploying a Spring Boot application, check:
Log levels
[ ] TRACE is disabled in production
[ ] DEBUG is used intentionally
[ ] INFO contains meaningful events
[ ] WARN represents recoverable problems
[ ] ERROR represents actual failures
Security
[ ] Passwords are never logged
[ ] Tokens are never logged
[ ] API keys are never logged
[ ] Sensitive headers are never logged
[ ] Sensitive payloads are not logged
Reliability
[ ] Log rotation is configured
[ ] Maximum file size is configured
[ ] Retention policy exists
[ ] Disk usage is monitored
Observability
[ ] Request ID exists
[ ] Correlation ID exists
[ ] Important business events are logged
[ ] External API failures are logged
[ ] Slow operations can be identified
Production
[ ] Structured logging is available
[ ] Logs can be centralized
[ ] Alerts can be created
[ ] Logs are searchable
[ ] Audit events are separated when required
37. What I Consider a Good Production Logging Strategy
For a modern Spring Boot backend, my preferred baseline would be:
Spring Boot
|
+--- SLF4J
|
+--- Logback
|
+--- Structured JSON
|
+--- Request ID / Correlation ID
|
+--- INFO as default
|
+--- DEBUG for troubleshooting
|
+--- ERROR for failures
|
+--- Audit logging for important actions
|
+--- Log rotation / retention
|
+--- Centralized log aggregation
|
+--- Monitoring + Alerting
For cloud deployments, I would generally prefer sending structured logs to a centralized platform rather than depending exclusively on local log files.
38. Final Thoughts
Logging is not something you add at the end of development.
It is part of backend architecture.
A production-ready application should make it easy to understand:
What happened?
When?
Where?
Who triggered it?
Which request?
Which service?
How long?
What failed?
Why?
The goal isn't to create millions of log lines.
The goal is to create useful signals.
A good production logging system gives developers confidence when everything is working and, more importantly, gives them the information they need when something goes wrong.
If your application is running in production, ask yourself:
"If this API fails at 3 AM, can I understand exactly what happened from the logs?"
If the answer is no, your logging strategy probably needs another iteration.
Top comments (0)