Logging (System log, Error log)

บทนำ: ความแตกต่างระหว่าง “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 ทุกอย่าง แต่เมื่อเกิดปัญหา มันคือ “ปลายด้าย” ที่ช่วยให้เราแก้ไขได้เร็ว