Careeroza
Logging with SLF4J
Why Not System.out.println?
System.out.println | Logger | |
|---|---|---|
| Timestamp, thread, class name | no | yes, automatically |
| Levels (turn DEBUG off in production) | no | yes |
| Write to files, rotate them, send to ELK/Grafana | no | yes, by configuration |
| Performance | always runs, synchronized | skipped cheaply when the level is off |
SLF4J + Logback
Spring Boot includes SLF4J (the logging API you code against) and Logback (the implementation that writes the logs) through spring-boot-starter-logging. Nothing to add.
package com.shopease.product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> all() {
List<Product> products = productService.findAll();
log.info("Returning {} products", products.size()); // {} = placeholder
log.debug("Product ids: {}", products.stream().map(Product::id).toList());
return products;
}
}Example
curl -s http://localhost:8080/api/products > /dev/nullExample output
2026-01-15T10:20:04.118+05:30 INFO 18244 --- [shopease] [nio-8080-exec-1] c.s.product.ProductController : Returning 4 products
Tip: Always use {} placeholders, not string concatenation: log.debug("x=" + x) builds the string even when DEBUG is off. With Lombok, @Slf4j on the class creates the log field for you.
Reading a Log Line
2026-01-15T10:20:04.118+05:30 INFO 18244 --- [shopease] [nio-8080-exec-1] c.s.product.ProductController : Returning 4 products | | | | | | | timestamp level PID app name thread logger (class) message
Placeholders in Plain Java
Example
How SLF4J fills {} placeholders, and why the message is not even built when the level is disabled:
public class Main {
static boolean debugEnabled = false;
static int messagesBuilt = 0;
static String format(String pattern, Object... args) {
messagesBuilt++;
StringBuilder sb = new StringBuilder();
int argIndex = 0, from = 0, at;
while ((at = pattern.indexOf("{}", from)) >= 0 && argIndex < args.length) {
sb.append(pattern, from, at).append(args[argIndex++]);
from = at + 2;
}
return sb.append(pattern.substring(from)).toString();
}
static void info(String pattern, Object... args) { System.out.println("INFO " + format(pattern, args)); }
static void debug(String pattern, Object... args) {
if (debugEnabled) System.out.println("DEBUG " + format(pattern, args)); // skipped cheaply
}
public static void main(String[] args) {
info("Order {} placed by user {} for Rs.{}", 101, 57, 2998);
debug("Cart details: {}", "a very big object");
info("Stock left for {}: {}", "Boat Rockerz 450", 23);
System.out.println("Messages built: " + messagesBuilt + " (the DEBUG one was never formatted)");
}
}Output
INFO Order 101 placed by user 57 for Rs.2998 INFO Stock left for Boat Rockerz 450: 23 Messages built: 2 (the DEBUG one was never formatted)