Something is broken in production. The app is running, users are reporting an issue, and you have no stack trace, no error message, and no obvious clue in the logs. Your first instinct is to add more logging — but the app is live, and redeploying just to change a log level feels like the wrong move.
The good news: Spring Boot gives you multiple ways to enable detailed debug logging without touching your code, without rebuilding the jar, and in some cases without even restarting the application. This guide covers all of them — from the simple properties-file approach to dynamic log level changes at runtime via Spring Boot Actuator.
Understanding Log Levels in Spring Boot
Spring Boot uses SLF4J as the logging API and Logback as the default implementation. Log levels are hierarchical — setting a level on a package applies to all classes within it:
| Level | When to Use | Verbosity |
|---|---|---|
ERROR | Application failures requiring immediate attention | Lowest |
WARN | Unexpected situations that aren’t failures (yet) | Low |
INFO | Normal application lifecycle events (default level) | Medium |
DEBUG | Detailed flow for diagnosing problems | High |
TRACE | Everything — every method call, every SQL bind parameter | Highest |
By default, Spring Boot logs at INFO level. Most production issues require DEBUG or TRACE on a specific package — not globally, which would flood your logs with thousands of irrelevant lines per second.
Method 1: Set Log Levels in application.properties
The simplest approach. Add these to your application.properties or application.yml:
# Enable DEBUG for your own application code only
logging.level.com.example.demo=DEBUG
# Enable DEBUG for Spring Web (shows request mapping, handler resolution)
logging.level.org.springframework.web=DEBUG
# Enable DEBUG for Hibernate SQL (shows every query)
logging.level.org.hibernate.SQL=DEBUG
# Enable TRACE for Hibernate bind parameters (shows actual values in queries)
logging.level.org.hibernate.orm.jdbc.bind=TRACE
# Enable DEBUG for Spring Security (shows filter chain decisions)
logging.level.org.springframework.security=DEBUG
In application.yml:
logging:
level:
com.example.demo: DEBUG
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
org.springframework.security: DEBUG
⚠️ Be specific with package names. Setting logging.level.root=DEBUG enables debug on the entire application — including every Spring internal, every Hibernate operation, every HTTP request detail — which can generate millions of log lines per minute in production and seriously degrade performance.
Method 2: Pass Log Levels at Startup (No Code Change)
If you can restart the app (or are doing a rolling deployment), you can pass log levels as command-line arguments without touching any properties files:
java -jar app.jar \
--logging.level.com.example.demo=DEBUG \
--logging.level.org.springframework.web=DEBUG
Or as JVM system properties:
java -Dlogging.level.com.example.demo=DEBUG -jar app.jar
This is ideal for Kubernetes or Docker deployments where you control the startup command via environment variables or config maps:
# In a Kubernetes ConfigMap or deployment environment
- name: LOGGING_LEVEL_COM_EXAMPLE_DEMO
value: "DEBUG"
Spring Boot automatically maps environment variables with underscores and uppercase to the equivalent dot-notation property names.
Method 3: Change Log Levels at Runtime Without Restarting (Actuator)
This is the most powerful option for production — you can change log levels on a running application with a single HTTP call, with no restart required.
Step 1: Add the Actuator Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Step 2: Expose the Loggers Endpoint
application.properties:
management.endpoints.web.exposure.include=loggers,health,info
management.endpoint.loggers.enabled=true
application.yml:
management:
endpoints:
web:
exposure:
include: loggers,health,info
endpoint:
loggers:
enabled: true
Step 3: Check the Current Log Level for a Package
curl http://localhost:8080/actuator/loggers/com.example.demo
Response:
{
"configuredLevel": null,
"effectiveLevel": "INFO"
}
Step 4: Change the Log Level Dynamically
curl -X POST \
http://localhost:8080/actuator/loggers/com.example.demo \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
No response body means it worked. Check your logs — debug output starts immediately. To revert back to INFO:
curl -X POST \
http://localhost:8080/actuator/loggers/com.example.demo \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "INFO"}'
🔒 Important: Secure the Actuator endpoints in production. Never expose /actuator publicly. Restrict access using Spring Security or network-level controls (only accessible from internal IPs or a bastion host):
application.properties:
# Bind actuator to a separate management port (internal only)
management.server.port=8081
management.server.address=127.0.0.1
application.yml:
management:
server:
port: 8081
address: 127.0.0.1
Method 4: Use Spring Profiles for Environment-Specific Log Levels
The cleanest long-term approach is to define log levels per environment using Spring profiles, so you never have to manually change them at all:
Using .properties files:
# application-dev.properties
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=DEBUG
spring.jpa.show-sql=true
# application-prod.properties
logging.level.com.example.demo=INFO
logging.level.org.springframework=WARN
spring.jpa.show-sql=false
Using .yml files:
# application-dev.yml
logging:
level:
com.example.demo: DEBUG
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
spring:
jpa:
show-sql: true
# application-prod.yml
logging:
level:
com.example.demo: INFO
org.springframework: WARN
spring:
jpa:
show-sql: false
Activate the correct profile at startup:
java -jar app.jar --spring.profiles.active=prod
Writing Effective Log Statements in Your Code
Enabling debug logging is only half the equation — your own log statements need to be useful. Use SLF4J’s parameterized logging (never string concatenation, which evaluates even when the log level is disabled):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order processOrder(Long orderId) {
log.info("Processing order: {}", orderId);
// DEBUG: useful detail, not needed in normal operation
log.debug("Fetching order from DB — orderId={}", orderId);
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> {
log.error("Order not found — orderId={}", orderId);
return new OrderNotFoundException(orderId);
});
log.debug("Order retrieved — status={}, items={}", order.getStatus(), order.getItems().size());
return order;
}
}
With Lombok, you can skip the boilerplate entirely:
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class OrderService {
public Order processOrder(Long orderId) {
log.info("Processing order: {}", orderId);
log.debug("Fetching order — orderId={}", orderId);
// ...
}
}
Production Debug Logging — Safety Checklist
- ✅ Enable
DEBUGonly on the specific package you’re diagnosing — neverroot - ✅ Use Actuator’s
/actuator/loggersendpoint for zero-downtime log level changes - ✅ Revert to
INFOimmediately after diagnosis — don’t leave DEBUG running in production - ✅ Never log sensitive data — passwords, tokens, PII, payment details — at any log level
- ✅ Secure Actuator endpoints — bind to management port, restrict to internal network only
- ✅ Use parameterized logging (
log.debug("id={}", id)) not string concatenation - ✅ Use profile-specific properties to keep dev/prod log levels separate by default
Conclusion
Debug logging in Spring Boot is a spectrum — from a simple properties change to dynamic runtime control via Actuator, there’s always an option that fits your deployment situation. For most production issues, the Actuator loggers endpoint is the right tool: no restart, no redeployment, targeted to a specific package, and fully reversible. Combine it with profile-based defaults and properly written log statements in your own code, and you’ll diagnose most production issues in minutes rather than hours.
