Chapitre 3: Logging avec SLF4J
Introduction
System.out.println() c'est bien pour déboguer, mais en production c'est inutilisable. SLF4J avec Logback vous donne un système de logging professionnel.
🎯 Objectifs du Chapitre
À la fin de ce chapitre, vous saurez:
- ✅ Utiliser SLF4J dans vos classes
- ✅ Comprendre les niveaux de log
- ✅ Écrire des logs efficaces
- ✅ Utiliser le MDC pour la traçabilité
- ✅ Éviter les erreurs courantes
1. SLF4J: La Façade de Logging
Architecture
Votre code
↓
SLF4J (API/Façade)
↓
Logback (Implémentation)
↓
Console / Fichier / Cloud
Avantage: Changer d'implémentation sans modifier le code.
Spring Boot l'inclut par défaut
<!-- Déjà inclus dans spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Contient: spring-boot-starter-logging (SLF4J + Logback) -->
2. Créer un Logger
Méthode Classique
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public User findById(Long id) {
log.info("Finding user with ID: {}", id);
return userRepository.findById(id).orElse(null);
}
}
Avec Lombok @Slf4j (Recommandé)
import lombok.extern.slf4j.Slf4j;
@Slf4j // ← Génère automatiquement le logger
@Service
public class UserService {
public User findById(Long id) {
log.info("Finding user with ID: {}", id);
return userRepository.findById(id).orElse(null);
}
}
3. Niveaux de Log
Hiérarchie (du moins au plus critique)
TRACE → DEBUG → INFO → WARN → ERROR
| Niveau | Usage | Exemple |
|---|---|---|
| TRACE | Détails très fins | "Entering method X with params..." |
| DEBUG | Informations de debug | "Calculated value: 42" |
| INFO | Événements normaux | "User logged in", "Order created" |
| WARN | Potentiels problèmes | "Cache miss", "Retrying connection" |
| ERROR | Erreurs | "Failed to save", "Connection refused" |
Utilisation
@Slf4j
@Service
public class OrderService {
public Order processOrder(Order order) {
log.trace("Entering processOrder with order: {}", order);
log.debug("Validating order items: {} items", order.getItems().size());
log.info("Processing order {} for user {}", order.getId(), order.getUserId());
if (order.getTotal() > 10000) {
log.warn("Large order detected: {} - Total: {}", order.getId(), order.getTotal());
}
try {
return saveOrder(order);
} catch (Exception e) {
log.error("Failed to process order {}: {}", order.getId(), e.getMessage(), e);
throw e;
}
}
}
4. Bonnes Pratiques de Logging
✅ Utiliser les Placeholders
// ✅ BON: Placeholders (performance optimale)
log.debug("Processing user {} with email {}", userId, email);
// ❌ MAUVAIS: Concaténation (toujours évaluée)
log.debug("Processing user " + userId + " with email " + email);
✅ Logger les Exceptions Correctement
try {
riskyOperation();
} catch (Exception e) {
// ✅ BON: Exception en dernier paramètre (affiche la stack trace)
log.error("Operation failed for user {}: {}", userId, e.getMessage(), e);
// ❌ MAUVAIS: Pas de stack trace
log.error("Operation failed: " + e.getMessage());
// ❌ MAUVAIS: e.printStackTrace() au lieu de logger
e.printStackTrace();
}
✅ Logs Utiles et Contextuels
// ✅ BON: Contexte suffisant
log.info("Order {} created for user {} - Total: {} - Items: {}",
order.getId(), order.getUserId(), order.getTotal(), order.getItems().size());
// ❌ MAUVAIS: Pas de contexte
log.info("Order created");
// ❌ MAUVAIS: Trop verbeux
log.info("Created order object with ID " + order.getId() + " and this order was created by user with ID " + order.getUserId() + "...");
✅ Éviter le Log Spam
// ❌ MAUVAIS: Log dans une boucle
for (Item item : items) {
log.debug("Processing item: {}", item); // 1000 logs si 1000 items!
}
// ✅ BON: Log résumé
log.debug("Processing {} items", items.size());
for (Item item : items) {
// traitement...
}
log.debug("Finished processing items");
5. Vérification du Niveau
Pour les Opérations Coûteuses
// Si le calcul est coûteux, vérifier le niveau d'abord
if (log.isDebugEnabled()) {
String expensiveDetails = calculateExpensiveDetails(order);
log.debug("Order details: {}", expensiveDetails);
}
// Alternative avec Java 8 Supplier (Logback 1.3+)
log.atDebug()
.addArgument(() -> calculateExpensiveDetails(order))
.log("Order details: {}");
6. MDC (Mapped Diagnostic Context)
Qu'est-ce que le MDC?
Le MDC permet d'ajouter du contexte à tous les logs d'une requête (userId, requestId, etc.).
Utilisation de Base
import org.slf4j.MDC;
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// Ajouter le contexte
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", getCurrentUserId());
chain.doFilter(request, response);
} finally {
// IMPORTANT: Toujours nettoyer!
MDC.clear();
}
}
}
Résultat dans les Logs
2024-01-15 10:30:00 [requestId=abc-123] [userId=42] INFO OrderService - Processing order
2024-01-15 10:30:01 [requestId=abc-123] [userId=42] INFO PaymentService - Payment processed
2024-01-15 10:30:01 [requestId=abc-123] [userId=42] INFO EmailService - Confirmation sent
Filter Complet avec MDC
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// Request ID
String requestId = request.getHeader("X-Request-ID");
if (requestId == null) {
requestId = UUID.randomUUID().toString().substring(0, 8);
}
MDC.put("requestId", requestId);
// User ID (si authentifié)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
MDC.put("userId", auth.getName());
}
// Path
MDC.put("path", request.getRequestURI());
// Ajouter le requestId dans la réponse
response.setHeader("X-Request-ID", requestId);
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
7. Logging dans les Différentes Couches
Controller
@Slf4j
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Order> create(@RequestBody OrderRequest request) {
log.info("Received order creation request: {} items", request.getItems().size());
Order order = orderService.create(request);
log.info("Order {} created successfully", order.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
}
Service
@Slf4j
@Service
public class OrderService {
public Order create(OrderRequest request) {
log.debug("Creating order for user {}", request.getUserId());
// Validation
validateItems(request.getItems());
// Calcul
BigDecimal total = calculateTotal(request.getItems());
log.debug("Calculated total: {}", total);
// Sauvegarde
Order order = orderRepository.save(toOrder(request, total));
log.info("Order {} saved with total {}", order.getId(), total);
return order;
}
}
Repository (optionnel)
// Les logs JPA/Hibernate sont généralement suffisants
// Mais pour des requêtes custom:
@Slf4j
@Repository
public class CustomOrderRepository {
public List<Order> findComplexQuery(Criteria criteria) {
log.debug("Executing complex query with criteria: {}", criteria);
List<Order> results = executeQuery(criteria);
log.debug("Query returned {} results", results.size());
return results;
}
}
8. Configuration Rapide
application.properties
# Niveau global
logging.level.root=INFO
# Niveau par package
logging.level.com.myapp=DEBUG
logging.level.com.myapp.repository=TRACE
# Afficher les requêtes SQL
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Désactiver des logs bruyants
logging.level.org.apache.catalina=WARN
logging.level.org.springframework.web=INFO
Niveaux Recommandés
| Environnement | Root | Application | Hibernate |
|---|---|---|---|
| Dev | INFO | DEBUG | DEBUG |
| Test | WARN | INFO | WARN |
| Prod | WARN | INFO | WARN |
9. Résumé
Créer un Logger
// Avec Lombok (recommandé)
@Slf4j
public class MyClass {
void method() {
log.info("Message");
}
}
// Sans Lombok
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
Niveaux
TRACE < DEBUG < INFO < WARN < ERROR
Bonnes Pratiques
// Placeholders
log.info("User {} logged in", userId);
// Exception avec stack trace
log.error("Failed: {}", e.getMessage(), e);
// MDC pour le contexte
MDC.put("requestId", "abc-123");
🎯 Prochaine Étape
Vous savez logger. Maintenant, configurons Logback pour contrôler où et comment les logs sont écrits.
➡️ Chapitre 4: Configuration Logback