บทนำ: ความแตกต่างระหว่าง “Print” กับ “Log”
เมื่อเขียน program ตัวแรก คุณอาจใช้ System.out.println() เพื่อ debug:
java// ❌ ไม่ดี: ใช้ println ทุกที่
System.out.println("User login at " + currentTime);
System.out.println("Database connected");
System.out.println("ERROR: Something went wrong!");
ปัญหา:
- ไม่มี timestamp – ไม่รู้เกิดเมื่อไร
- ไม่มี severity level – ไม่รู้ความสำคัญ (warning vs error)
- ยากต่อการค้นหา – ต้องค้นหาใน output ทั้งหมด
- ไม่มี context – ไม่รู้จาก class ไหน
- ยากต่อการ rotate – output ไฟล์ใหญ่เกิน
- Production ปั่น output เยอะ – ทำให้ช้า
Logging Framework = ระบบที่ออกแบบมาให้ record events ของ program อย่างมืออาชีพ
Logging vs System.out: เข้าใจความแตกต่าง
ตัวอย่างที่ 1: พิจารณา Logging Levels
java// ✗ ไม่ดี: ไม่มี distinction ระหว่าง severity
System.out.println("Server started"); // ← INFO? DEBUG?
System.out.println("Cache miss"); // ← DEBUG?
System.out.println("Connection timeout"); // ← WARNING? ERROR?
System.out.println("Database down"); // ← ERROR? CRITICAL?
// ✓ ดี: ชัดเจนว่า severity ไหน
logger.info("Server started on port 8080");
logger.debug("Cache miss for key: user_123");
logger.warn("Connection timeout after 30 seconds");
logger.error("Database connection failed", exception);
logger.fatal("Entire system is down");
Logging Levels (จาก low to high severity)
textDEBUG → Development info, very detailed
INFO → General information, normal operations
WARN → Warning, something unexpected
ERROR → Error, but recoverable
FATAL → Critical, system cannot continue
Java Logging Frameworks ที่ใช้บ่อย
text┌─────────────────────────────────────┐
│ Logging API │
├─────────────────────────────────────┤
│ java.util.logging (Built-in) │ ← Java SE
│ SLF4J (Simple Logging Facade) │ ← Popular abstraction
├─────────────────────────────────────┤
│ Logging Implementations │
├─────────────────────────────────────┤
│ Log4j / Log4j2 │ ← Apache (fast, powerful)
│ Logback │ ← Replacement for Log4j
│ java.util.logging │ ← Built-in
└─────────────────────────────────────┘
ตัวอย่างที่ 2: เทียบ System.out vs Logger
java// ❌ WITHOUT LOGGING
public class UserService {
public User getUserById(int id) {
System.out.println("Getting user: " + id);
try {
User user = database.query(id);
System.out.println("User found: " + user.getName());
return user;
} catch (SQLException e) {
System.out.println("ERROR: " + e.getMessage());
return null;
}
}
}
// Output (impossible to parse):
// Getting user: 123
// User found: John Doe
// Getting user: 456
// ERROR: Connection timeout
// Getting user: 789
// ← Which error corresponds to which user? Timeline unclear
// ✓ WITH LOGGING
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger logger =
LoggerFactory.getLogger(UserService.class);
public User getUserById(int id) {
logger.debug("Fetching user with ID: {}", id);
try {
User user = database.query(id);
logger.info("Successfully retrieved user: {} (ID: {})",
user.getName(), id);
return user;
} catch (SQLException e) {
logger.error("Failed to retrieve user with ID: {}",
id, e);
return null;
}
}
}
// Output (structured, with timestamps and levels):
// 2025-11-06 14:20:15.123 DEBUG [UserService] Fetching user with ID: 123
// 2025-11-06 14:20:15.456 INFO [UserService] Successfully retrieved user: John Doe (ID: 123)
// 2025-11-06 14:20:16.123 DEBUG [UserService] Fetching user with ID: 456
// 2025-11-06 14:20:16.789 ERROR [UserService] Failed to retrieve user with ID: 456
// java.sql.SQLException: Connection timeout
// at DatabaseConnection.query(DatabaseConnection.java:45)
// ← Clear, structured, easy to parse and debug
ตัวอย่างที่ 3: Logging with SLF4J & Logback
Setup: pom.xml (Maven)
xml<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
</dependency>
Configuration: logback.xml
xml<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console appender -->
<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>
<!-- File appender for all logs -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level
%logger{36} - %msg%n
</pattern>
</encoder>
<!-- Rotate daily, keep 30 days -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- File appender for errors only -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error.log</file>
<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%xEx
</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
</appender>
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
<!-- Logger for specific package (more verbose) -->
<logger name="com.example.userservice" level="DEBUG" />
</configuration>
Code Example: Using Logger
javaimport org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger logger =
LoggerFactory.getLogger(OrderService.class);
private OrderRepository orderRepository;
private PaymentService paymentService;
public Order placeOrder(OrderRequest request)
throws InvalidOrderException, PaymentException {
// DEBUG: Entry point
logger.debug("Attempting to place order for customer: {}",
request.getCustomerId());
try {
// Validate request
if (request == null || request.getItems().isEmpty()) {
logger.warn("Invalid order request from customer: {}",
request != null ? request.getCustomerId() : "unknown");
throw new InvalidOrderException("Order cannot be empty");
}
logger.info("Order validation successful, {} items",
request.getItems().size());
// Create order
Order order = orderRepository.create(request);
logger.debug("Order created with ID: {}", order.getId());
// Process payment
logger.debug("Processing payment for order: {}", order.getId());
try {
paymentService.charge(order.getTotalAmount(),
request.getPaymentMethod());
logger.info("Payment successful for order: {}", order.getId());
} catch (PaymentException e) {
logger.error("Payment failed for order: {}, reverting...",
order.getId(), e);
orderRepository.rollback(order.getId());
throw e;
}
// Update order status
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.update(order);
logger.info("Order {} confirmed and ready for shipment",
order.getId());
return order;
} catch (InvalidOrderException e) {
logger.error("Invalid order request", e);
throw e;
} catch (Exception e) {
logger.error("Unexpected error while placing order", e);
throw new RuntimeException("Order placement failed", e);
}
}
}
// Output example:
// 2025-11-06 14:25:30.123 [main] DEBUG OrderService -
// Attempting to place order for customer: 1001
// 2025-11-06 14:25:30.234 [main] INFO OrderService -
// Order validation successful, 3 items
// 2025-11-06 14:25:30.345 [main] DEBUG OrderService -
// Order created with ID: ORD-2025-00123
// 2025-11-06 14:25:30.456 [main] DEBUG OrderService -
// Processing payment for order: ORD-2025-00123
// 2025-11-06 14:25:30.678 [main] INFO OrderService -
// Payment successful for order: ORD-2025-00123
// 2025-11-06 14:25:30.789 [main] INFO OrderService -
// Order ORD-2025-00123 confirmed and ready for shipment
ตัวอย่างที่ 4: Structured Logging for Analysis
System Log vs Error Log: Strategic Placement
javapublic class UserRepository {
private static final Logger logger =
LoggerFactory.getLogger(UserRepository.class);
private static final Logger errorLogger =
LoggerFactory.getLogger("error");
public User getUserById(int id) throws UserNotFoundException {
logger.debug("Querying user with ID: {}", id);
try {
// Query database
String query = "SELECT * FROM users WHERE id = ?";
User user = executeQuery(query, id);
if (user == null) {
// This is expected sometimes
logger.warn("User not found with ID: {}", id);
throw new UserNotFoundException("User " + id + " not found");
}
logger.info("User retrieved: {} (ID: {})", user.getEmail(), id);
return user;
} catch (SQLException e) {
// Database error - serious problem
errorLogger.error("Database error while fetching user {}",
id, e);
logger.error("Failed to retrieve user: {}", id);
throw new RuntimeException(
"Failed to retrieve user from database", e
);
}
}
public void createUser(User user) {
logger.debug("Creating user: {}", user.getEmail());
try {
String query = "INSERT INTO users (...) VALUES (...)";
executeUpdate(query, user);
logger.info("User created successfully: {}", user.getEmail());
} catch (SQLException e) {
errorLogger.error("Failed to create user: {}",
user.getEmail(), e);
logger.error("User creation failed");
throw new RuntimeException("Failed to create user", e);
} catch (Exception e) {
errorLogger.error("Unexpected error during user creation", e);
throw e;
}
}
}
// File structure:
// logs/
// ├── application.log ← All logs (DEBUG, INFO, WARN, ERROR)
// ├── application.2025-11-06.1.log
// ├── application.2025-11-05.1.log
// └── error.log ← ERROR logs only
// └── error.2025-11-06.1.log
ตัวอย่างที่ 5: Real-World Logging Strategy
Multi-Layer Logging: Tracking Request Through System
java// ==== Controller Layer ====
@RestController
public class OrderAPI {
private static final Logger logger =
LoggerFactory.getLogger(OrderAPI.class);
private OrderService orderService;
@PostMapping("/orders")
public ResponseEntity<OrderDTO> createOrder(
@RequestBody OrderRequest request,
HttpServletRequest httpRequest) {
String requestId = UUID.randomUUID().toString();
logger.info("Received order request [ID: {}] from IP: {}",
requestId, httpRequest.getRemoteAddr());
try {
Order order = orderService.placeOrder(request);
logger.info("Order created successfully [ID: {}]", requestId);
return ResponseEntity.ok(mapToDTO(order));
} catch (InvalidOrderException e) {
logger.warn("Invalid order [ID: {}]: {}", requestId, e.getMessage());
return ResponseEntity.badRequest().build();
} catch (PaymentException e) {
logger.error("Payment failed [ID: {}]", requestId, e);
return ResponseEntity.status(402).build();
} catch (Exception e) {
logger.error("Unexpected error processing order [ID: {}]",
requestId, e);
return ResponseEntity.status(500).build();
}
}
}
// ==== Service Layer ====
public class OrderService {
private static final Logger logger =
LoggerFactory.getLogger(OrderService.class);
public Order placeOrder(OrderRequest request) {
logger.debug("Service: Starting order placement");
// Validate
validateOrder(request);
logger.debug("Service: Order validation passed");
// Create
Order order = new Order(request);
logger.debug("Service: Order object created");
// Process payment
try {
paymentService.charge(order.getTotalAmount());
logger.info("Service: Payment processed");
} catch (PaymentException e) {
logger.error("Service: Payment failed", e);
throw e;
}
return order;
}
}
// ==== Repository Layer ====
public class OrderRepository {
private static final Logger logger =
LoggerFactory.getLogger(OrderRepository.class);
public void save(Order order) {
logger.debug("Repository: Saving order to database");
try {
String sql = "INSERT INTO orders ...";
database.execute(sql, order);
logger.debug("Repository: Order saved with ID: {}", order.getId());
} catch (SQLException e) {
logger.error("Repository: Database error saving order", e);
throw new RuntimeException("Failed to save order", e);
}
}
}
// Log output (following request through layers):
// [REST] 2025-11-06 14:30:00.123 INFO OrderAPI -
// Received order request [ID: abc-123] from IP: 192.168.1.1
// [REST] 2025-11-06 14:30:00.234 DEBUG OrderService -
// Service: Starting order placement
// [REST] 2025-11-06 14:30:00.345 DEBUG OrderService -
// Service: Order validation passed
// [REST] 2025-11-06 14:30:00.456 DEBUG OrderService -
// Service: Order object created
// [REST] 2025-11-06 14:30:00.567 DEBUG OrderRepository -
// Repository: Saving order to database
// [REST] 2025-11-06 14:30:00.678 DEBUG OrderRepository -
// Repository: Order saved with ID: ORD-2025-00456
// [REST] 2025-11-06 14:30:00.789 INFO OrderService -
// Service: Payment processed
// [REST] 2025-11-06 14:30:00.890 INFO OrderAPI -
// Order created successfully [ID: abc-123]
ตัวอย่างที่ 6: Common Logging Patterns
Pattern 1: Entry & Exit Logging
javapublic class DatabaseService {
private static final Logger logger =
LoggerFactory.getLogger(DatabaseService.class);
public List<User> getAllUsers() {
logger.debug("ENTRY: getAllUsers()");
try {
List<User> users = database.query("SELECT * FROM users");
logger.debug("EXIT: getAllUsers() returned {} users",
users.size());
return users;
} catch (Exception e) {
logger.error("EXIT-ERROR: getAllUsers() threw exception", e);
throw e;
}
}
}
Pattern 2: Performance Monitoring
javapublic class PerformanceMonitor {
private static final Logger logger =
LoggerFactory.getLogger(PerformanceMonitor.class);
public void executeSlowOperation() {
long startTime = System.currentTimeMillis();
logger.debug("Starting slow operation");
try {
slowOperation();
} finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > 1000) {
logger.warn("Slow operation took {} ms", duration);
} else {
logger.debug("Operation completed in {} ms", duration);
}
}
}
}
Pattern 3: Sensitive Data Masking
javapublic class AuthService {
private static final Logger logger =
LoggerFactory.getLogger(AuthService.class);
public void authenticate(String username, String password) {
// ✗ WRONG: Don't log passwords!
// logger.info("Attempting login for {} with password: {}",
// username, password);
// ✓ RIGHT: Log only safe info
logger.info("Attempting login for user: {}", username);
if (validateCredentials(username, password)) {
logger.info("User {} authenticated successfully", username);
} else {
logger.warn("Authentication failed for user: {}", username);
}
}
}
Best Practices: Logging Checklist
text✓ DO:
☑ Use appropriate log level
☑ Include context (user ID, request ID, etc.)
☑ Log at entry/exit of important methods
☑ Log exceptions with stack trace
☑ Use placeholders {} for parameters
☑ Rotate log files (daily/by size)
☑ Store error logs separately
☑ Use structured logging for analysis
☑ Don't log sensitive data
☑ Include timestamps and thread info
✗ DON'T:
☐ Use System.out.println() in production
☐ Log every single variable
☐ Log passwords, tokens, API keys
☐ Use string concatenation in log (use {})
☐ Lose exception stack traces
☐ Log at wrong level (INFO when should be DEBUG)
☐ Mix multiple concerns in one log message
☐ Keep logs indefinitely (storage!)
สรุป
Logging ไม่ใช่แค่ “พิมพ์ debug info” แต่เป็นการออกแบบ observable system:
ทำไมต้อง Logging Framework:
- Structured – มี timestamp, level, context
- Configurable – เปลี่ยน log level โดยไม่ต้อง recompile
- Rotatable – เก่าสุดไป, เก็บไม่เต็มดิสก์
- Searchable – ค้นหาปัญหาได้ง่าย
- Production-ready – ไม่ช่วยชี้ให้คนเห็น output
Strategy ที่ดี:
- DEBUG → Development investigation
- INFO → Important business events
- WARN → Unexpected but recoverable
- ERROR → Serious problems
- Separate error logs → สำหรับ alert/monitoring
เมื่อใช้ Logging อย่างถูกต้อง:
- Debug ได้ง่าย – ปัญหา trace ได้จากไฟล์ log
- Monitor ได้ – เห็น system health realtime
- Comply ได้ – มีแหล่ง audit trail
- Professional – ดูเหมือน enterprise application
Logging ที่ดีคือ “ตาที่มองไม่เห็น” ของ system – มันอยู่เงียบ ๆ record ทุกอย่าง แต่เมื่อเกิดปัญหา มันคือ “ปลายด้าย” ที่ช่วยให้เราแก้ไขได้เร็ว
