Spring Core & Boot/Logging cơ bản — SLF4J facade, Logback, log level và hierarchy
39/41
Bài 39 / 41~11 phútConfig, Profiles & LoggingMiễn phí lượt xem

Logging cơ bản — SLF4J facade, Logback, log level và hierarchy

Boot pre-wire 4-layer logging stack: SLF4J facade, Logback implementation, bridge libs. Bài này giải thích vì sao facade pattern tồn tại, cách 5 log level + hierarchy theo package hoạt động, và cấu hình qua logging.level.* property.

TL;DR: Boot logging stack gồm 4 layer — SLF4J facade (API duy nhất code app dùng), Logback implementation, bridge libs redirect log từ JUL/Log4j legacy, và output. Facade pattern tồn tại để đổi implementation không sửa code: swap Logback sang Log4j2 là đổi 2 dòng pom, không chạm business code. 5 log level từ TRACE đến ERROR tạo thành hierarchy theo package — set com.example: DEBUG bật toàn bộ package đó, child override parent bằng config cụ thể hơn. Tại sao dùng log.info("{}", x) thay log.info("val=" + x): placeholder lazy — nếu level INFO tắt, string concat không bao giờ chạy.

Mỗi app Boot in log từ giây đầu — không cần config. Đó là vì Boot đã pre-wire toàn bộ logging stack qua spring-boot-starter-logging. Bài này bóc cơ chế đó: 4 layer trong stack là gì, façade pattern giải quyết vấn đề gì, log level + hierarchy hoạt động ra sao, và cấu hình qua property.

Bài trước — Profile groups & patterns — giải thích cách Boot chọn config theo môi trường. Bài này nối tiếp: logging cũng cấu hình theo môi trường, và hiểu stack là nền để Structured logging ở bài sau thêm JSON output cho production.

1. Stack logging Boot — 4 layer

Boot không tự code logger. Nó tích hợp sẵn 4 component thành một stack:

flowchart TB
    App["App code<br/>(OrderService, Controller...)"]
    SLF4J["SLF4J<br/>Simple Logging Facade for Java<br/>(API thuan tuy)"]
    Logback["Logback<br/>(implementation default)"]
    Output["Console / File / Cloud"]
    Bridges["Bridge libs<br/>jul-to-slf4j (java.util.logging)<br/>log4j-to-slf4j (Log4j 1.x)<br/>jcl-over-slf4j (commons-logging)"]
    ThirdParty["Thu vien 3rd-party<br/>(Hibernate, Tomcat, Jackson...)"]

    App -->|"log.info(...)"| SLF4J
    SLF4J -->|"binding"| Logback
    Logback --> Output
    ThirdParty --> Bridges
    Bridges --> SLF4J

Ba layer đầu:

  • SLF4J (Simple Logging Facade for Java): interface thuần tuý — Logger, LoggerFactory. Code app chỉ dùng SLF4J, không import Logback trực tiếp.
  • Logback: implementation mặc định Boot chọn. Nhận log từ SLF4J, format, ghi ra console/file.
  • Bridge libs: capture log từ thư viện cũ (Tomcat dùng java.util.logging, Hibernate dùng Log4j 1.x, Spring cũ dùng Commons Logging) rồi redirect qua SLF4J. Nhờ đó toàn bộ log từ mọi thư viện đều unified qua Logback.

spring-boot-starter-logging (transitive của mọi spring-boot-starter-*) pull cả 4 layer:

spring-boot-starter-logging
├── logback-classic        (implementation)
├── logback-core
├── slf4j-api              (facade API)
├── jul-to-slf4j           (bridge JUL)
├── log4j-to-slf4j         (bridge Log4j)
└── jcl-over-slf4j         (bridge Commons Logging)

Không cần khai báo gì — add bất kỳ starter nào thì logging stack sẵn sàng.

2. Tại sao cần facade — vấn đề không có SLF4J

Trước SLF4J ra đời (2004), mỗi thư viện Java dùng logging framework riêng: Hibernate dùng Log4j, Spring dùng Commons Logging, Tomcat dùng JUL. App muốn log phải chọn 1 trong số đó — và nếu đổi framework log, phải sửa mọi import trong codebase.

SLF4J giải quyết bằng facade pattern (hay còn gọi abstraction layer): code viết theo interface chung của SLF4J, implementation thật (Logback, Log4j2...) chỉ khai báo ở pom/gradle, không xuất hiện trong source.

flowchart LR
    Code["App code<br/>import org.slf4j.Logger"]
    API["SLF4J API<br/>(interface)"]
    LB["Logback<br/>(binding A)"]
    L4J["Log4j2<br/>(binding B)"]

    Code --> API
    API -.->|"runtime binding"| LB
    API -.->|"swap pom.xml"| L4J

Hệ quả thực tế: đổi từ Logback sang Log4j2 chỉ cần:

  1. Exclude spring-boot-starter-logging trong pom.
  2. Add spring-boot-starter-log4j2.

Không một dòng business code nào thay đổi — vì tất cả đều import org.slf4j.Logger, không phải ch.qos.logback.Logger hay org.apache.log4j.Logger.

Nguyên tắc đằng sau facade

Facade pattern tách biệt hợp đồng sử dụng (API) khỏi cài đặt cụ thể (implementation). Code viết theo hợp đồng — ai implement không quan trọng. Đây là cùng nguyên tắc với BeanFactory interface trong Spring container, hay DataSource interface trong JDBC.

3. Dùng SLF4J đúng cách

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void place(Order order) {
        log.debug("Placing order {}", order.id());
        log.info("Order placed: id={}, total={}", order.id(), order.total());

        try {
            payment.charge(order);
        } catch (PaymentException e) {
            log.error("Payment failed for order {}", order.id(), e);  // exception la param cuoi
        }
    }
}

Hoặc dùng Lombok @Slf4j để tự generate dòng khai báo logger:

@Slf4j   // generate: private static final Logger log = LoggerFactory.getLogger(OrderService.class);
@Service
public class OrderService {
    public void place(Order order) {
        log.info("Order placed: id={}", order.id());
    }
}

3.1 Tại sao placeholder {} thay vì string concat

Đây là lý do cơ chế, không chỉ style:

// SAI: string concat chay ngay ca khi DEBUG tat
log.debug("Processing order " + order.id() + " for user " + order.userId());

// DUNG: placeholder lazy — chi substitute khi DEBUG bat
log.debug("Processing order {} for user {}", order.id(), order.userId());

Khi level là INFO (DEBUG tắt), dòng thứ nhất vẫn tạo string trung gian rồi bỏ đi — lãng phí CPU cho mỗi call trong hot path. Dòng thứ hai: SLF4J kiểm tra level trước, nếu DEBUG không bật thì không gọi toString() trên các arguments, không tạo string nào cả.

Trong loop xử lý nghìn record/giây, sự khác biệt này có thể đo được.

3.2 Exception phải là param cuối

// SAI: chi in message, MAT stack trace
log.error("Payment failed: " + e.getMessage());
log.error("Payment failed: {}", e.getMessage());

// DUNG: SLF4J detect param cuoi la Throwable, log ca stack trace
log.error("Payment failed for order {}", order.id(), e);

SLF4J có xử lý đặc biệt: nếu param cuối cùng là Throwable, nó log toàn bộ stack trace. Khi debug bug production, stack trace là thông tin không thể thay thế — dùng hai cách sai trên sẽ chỉ thấy một dòng message, không biết lỗi từ đâu.

4. Năm log level và hierarchy theo package

4.1 Năm level

SLF4J định nghĩa 5 level theo thứ tự từ chi tiết nhất đến quan trọng nhất:

LevelKhi dùngProduction default
TRACEChi tiết đặc biệt — entry/exit từng method, giá trị từng iterationTắt
DEBUGDebug flow — giá trị biến, nhánh logic, SQL parameterTắt
INFOSự kiện business quan trọng — order placed, user login, job startedBật
WARNVấn đề tiềm năng nhưng app vẫn chạy — retry thành công, fallback dùng cacheBật
ERRORLỗi cần điều tra — exception không xử lý được, request thất bạiBật

Quy tắc: khi level được set là INFO, chỉ INFO/WARN/ERROR đi qua. TRACE và DEBUG bị chặn trước khi format.

4.2 Hierarchy theo package

Logger có hierarchy tự động theo tên package — child inherit level từ parent gần nhất có config:

# application.yml
logging:
  level:
    root: WARN                              # default cho tat ca logger
    com.example: INFO                       # moi class trong com.example
    com.example.payment: DEBUG              # override — payment DEBUG
    com.example.payment.StripeGateway: INFO # override lai — class cu the INFO
    org.hibernate.SQL: DEBUG                # log SQL query
    org.springframework.web: WARN           # giam noise Spring web

Cách resolve level cho com.example.payment.StripeGateway:

  1. Tìm config cho com.example.payment.StripeGateway → có, INFO. Apply.

Cách resolve cho com.example.payment.PaypalGateway:

  1. Tìm com.example.payment.PaypalGateway → không có.
  2. Walk up: com.example.payment → có, DEBUG. Apply.
flowchart TB
    Root["root: WARN"]
    Pkg["com.example: INFO"]
    Pay["com.example.payment: DEBUG"]
    Stripe["com.example.payment.StripeGateway: INFO<br/>(explicit override)"]
    Paypal["com.example.payment.PaypalGateway<br/>(no config, inherit DEBUG)"]
    Order["com.example.order.OrderService<br/>(no config, inherit INFO)"]

    Root --> Pkg
    Pkg --> Pay
    Pkg --> Order
    Pay --> Stripe
    Pay --> Paypal
Pattern hay dung

com.example: INFO làm baseline cho toàn bộ app code. org.hibernate.SQL: DEBUG + org.hibernate.orm.jdbc.bind: TRACE để log SQL query + parameter khi debug N+1 hoặc query phức tạp. org.springframework.web: WARN để tắt verbose Spring internal log.

4.3 Cấu hình qua logging.level.*

logging:
  level:
    root: INFO
    com.example: DEBUG
    com.example.security: INFO           # tiet che hon — security class log it hon
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE   # log SQL parameter

Không cần logback-spring.xml cho việc này — property đủ cho 90% case điều chỉnh level.

5. Cơ chế bên dưới — vì sao log level check nhanh

Khi bạn gọi log.debug("..."), Logback không lập tức format và ghi — nó kiểm tra level trước:

flowchart TB
    Call["log.debug(msg, args...)"]
    Check{"Logger.isDebugEnabled()?<br/>(compare integer level)"}
    Skip["return ngay, khong lam gi"]
    Fmt["format message<br/>(substitute placeholder)"]
    Append["appender: ghi Console/File"]

    Call --> Check
    Check -->|"No (level > DEBUG)"| Skip
    Check -->|"Yes"| Fmt
    Fmt --> Append

Level được lưu dưới dạng integer (TRACE=0, DEBUG=10, INFO=20, WARN=30, ERROR=40). Kiểm tra level là so sánh integer — O(1), gần như không có overhead. Đây là lý do placeholder lazy an toàn: nếu level check fail, SLF4J không gọi bất kỳ .toString() nào trên arguments.

Logger hierarchy cũng được cache sau lần resolve đầu tiên — không phải walk up tree mỗi lần log.

6. Pitfall phổ biến

Nhầm 1 — String concat trong log call:

log.debug("user " + userId + " action " + action);     // concat luon, ngay ca khi DEBUG tat

✅ Placeholder lazy:

log.debug("user {} action {}", userId, action);

Nhầm 2 — Log exception thiếu stack trace:

log.error("Payment failed: " + e.getMessage());     // mat stack trace
log.error("Payment failed: {}", e);                 // toString() cua exception, van mat stack trace

✅ Exception là param cuối:

log.error("Payment failed for order {}", order.id(), e);   // log + full stack trace

Nhầm 3 — Tưởng log.info(...) có overhead khi level INFO:

Có lẽ bạn nghĩ: "INFO là level mặc định, nên log.info(...) chắc tốn kém?" Không — INFO call có overhead gần bằng 0 khi level INFO bật, chỉ format + write. Overhead cần lo là log.debug(...) trong hot path khi level INFO (vì tốn chi phí kiểm tra + placeholder không chạy nhưng args vẫn được đánh giá nếu dùng concat).

Nhầm 4 — Logger không static final:

private Logger log = LoggerFactory.getLogger(OrderService.class);  // SAI — tao moi moi instance

✅ Logger là stateless, dùng chung an toàn giữa các instance:

private static final Logger log = LoggerFactory.getLogger(OrderService.class);

LoggerFactory.getLogger() cache logger theo tên — static final đảm bảo chỉ lookup một lần per class.

Liên hệ các bài khác

  • Profile groups & patterns: config logging hay thay đổi theo profile — logging.level.com.example: DEBUG ở profile dev, WARN ở profile prod. Hiểu profile trước giúp tổ chức logging.level.* config hợp lý.
  • Structured logging: bài tiếp theo đi sâu vào JSON log cho production (Boot 3.4 ECS format), MDC correlation ID, và dynamic log level qua Actuator. Cơ chế 4-layer stack + hierarchy bài này là nền để hiểu structured logging build trên đó.
  • Externalized configuration: logging.level.* là một trong hàng trăm property Boot expose qua application.yml. Bài đó giải thích cơ chế @ConfigurationProperties và thứ tự ưu tiên nguồn config — quan trọng khi override logging.level qua environment variable trong K8s.

Tóm tắt

  • Boot pre-wire 4-layer logging stack qua spring-boot-starter-logging: SLF4J facade → Logback impl → bridge libs (JUL/Log4j/Commons Logging) → output.
  • Facade pattern (SLF4J): code chỉ import org.slf4j.Logger — đổi implementation không sửa business code.
  • Dùng SLF4J đúng: static final logger, placeholder {} thay concat (lazy — chỉ substitute khi level bật), exception là param cuối (log.error("msg", e) để có stack trace).
  • 5 level TRACE/DEBUG/INFO/WARN/ERROR. Production default: INFO+. DEBUG tắt khi deploy — dùng cho diagnostics tạm thời.
  • Hierarchy theo package: logger child inherit level từ parent gần nhất có config. Set com.example: INFO làm baseline; override package/class cụ thể.
  • Cấu hình qua logging.level.* property — không cần logback-spring.xml cho 90% case điều chỉnh level.

Tự kiểm tra

Tự kiểm tra
Q1
Tại sao code app nên import org.slf4j.Logger thay vì ch.qos.logback.classic.Logger trực tiếp? Nếu sau này muốn đổi sang Log4j2 thì thay đổi gì?

SLF4J là facade — interface trung lập, tách biệt code khỏi implementation cụ thể. Code import org.slf4j.Logger không phụ thuộc Logback hay Log4j2.

Nếu import ch.qos.logback.classic.Logger trực tiếp, mọi class business sẽ bị ràng buộc vào Logback. Khi đổi sang Log4j2, phải tìm và sửa tất cả import đó.

Với SLF4J, để đổi sang Log4j2 chỉ cần 2 thay đổi trong pom.xml: exclude spring-boot-starter-logging, thêm spring-boot-starter-log4j2. Không một dòng business code nào thay đổi — đây là lợi ích cốt lõi của facade pattern.

Tương tự nguyên tắc với DataSource interface JDBC (code không biết HikariCP hay C3P0 bên dưới) hay BeanFactory interface Spring container.

Q2
Giải thích tại sao log.debug("user " + userId + " did " + action) tệ hơn log.debug("user {} did {}", userId, action) về performance. Điều gì xảy ra ở cấp JVM cho mỗi cách khi log level là INFO?

String concat version: dù log level là INFO (DEBUG tắt), JVM vẫn phải:

  1. Gọi userId.toString()action.toString().
  2. Tạo StringBuilder, append các phần, tạo chuỗi trung gian.
  3. SLF4J mới check level → thấy DEBUG tắt → bỏ string vừa tạo.

Kết quả: tạo object rồi bỏ đi, gây áp lực GC. Trong loop xử lý nghìn record/giây, lãng phí đáng kể.

Placeholder version: SLF4J check level trước — level INFO thì DEBUG không bật → return ngay lập tức, không gọi toString(), không tạo string nào.

Level check là so sánh integer (DEBUG=10 < INFO=20) — O(1), gần như không tốn gì. Chỉ khi level enabled, SLF4J mới format message.

Quy tắc: luôn dùng placeholder {}. Đặc biệt quan trọng cho debug/trace call trong hot path.

Q3
Cho config sau, active log level của từng logger là gì?
root: WARN, com.example: INFO, com.example.order: DEBUG, com.example.order.OrderService: WARN
Liệt kê level của: com.example.UserService, com.example.order.CartService, com.example.order.OrderService, org.springframework.web.DispatcherServlet.

Hierarchy resolver: tìm config exact → nếu không có, walk up package đến root.

  • com.example.UserService: không có config explicit → lên com.exampleINFO.
  • com.example.order.CartService: không có config explicit → lên com.example.orderDEBUG.
  • com.example.order.OrderService: có config explicit → WARN.
  • org.springframework.web.DispatcherServlet: không match bất kỳ package nào → root → WARN.

Lưu ý com.example.order.OrderService: dù parent com.example.order là DEBUG, config explicit trên class override lên WARN — child luôn thắng parent khi có config riêng.

Pattern thực tế: dùng hierarchy để set DEBUG cho toàn bộ package đang debug, rồi override tắt bớt class quá verbose. Linh hoạt hơn nhiều so với phải config từng class.

Q4
Đoạn code sau có 2 lỗi logging. Chỉ ra và giải thích hậu quả của từng lỗi.

@Service public class PaymentService {
private Logger log = LoggerFactory.getLogger(PaymentService.class);
public void charge(Order o) {
try { gateway.charge(o); }
catch (Exception e) { log.error("Charge failed: " + e.getMessage()); }
}
}

Lỗi 1: Logger không static final.

Hậu quả: LoggerFactory.getLogger() được gọi mỗi lần tạo instance PaymentService. Dù Logback cache logger nội bộ, mỗi instance vẫn giữ một field riêng trong heap. Với singleton Bean Spring thì ít ảnh hưởng, nhưng là anti-pattern — logger là stateless và an toàn cho đa luồng, nên phải static final: một instance duy nhất per class.

Lỗi 2: String concat + chỉ log message, mất stack trace.

log.error("Charge failed: " + e.getMessage()) có hai vấn đề:

  1. String concat + e.getMessage(): tạo string ngay cả khi level tắt — như phân tích ở câu 2.
  2. Chỉ log e.getMessage(): mất toàn bộ stack trace. Khi debug production, chỉ biết "Charge failed: timeout" mà không biết lỗi xảy ra ở dòng nào, call chain ra sao.

Cách sửa:

private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

log.error("Charge failed for order {}", o.id(), e);

SLF4J detect tham số cuối là Throwable → tự động log message + full stack trace.

Q5
Bridge libs trong stack logging Boot (jul-to-slf4j, log4j-to-slf4j, jcl-over-slf4j) giải quyết vấn đề gì? Điều gì xảy ra nếu không có chúng?

Vấn đề: các thư viện 3rd-party sử dụng logger riêng — Tomcat dùng java.util.logging (JUL), Hibernate dùng Log4j 1.x, Spring cũ và Apache Commons dùng Commons Logging. Mỗi framework in log theo cơ chế riêng.

Nếu không có bridge libs: log từ app business đi qua SLF4J → Logback (format đẹp, level control được), nhưng log từ Tomcat vẫn in qua JUL (format khác, level config khác), log từ Hibernate in qua Log4j (lại format khác). Kết quả: log console lộn xộn, không unified — khó đọc, không filter được bằng logging.level.

Bridge libs giải quyết: thay thế implementation của framework cũ bằng wrapper redirect sang SLF4J. jul-to-slf4j cài SLF4JBridgeHandler vào JUL handler chain; jcl-over-slf4j thay class org.apache.commons.logging.Log bằng implementation forward sang SLF4J.

Kết quả: toàn bộ log từ mọi thư viện trong JVM đều chảy vào SLF4J → Logback. Một nơi config level, một pattern, một output. Đây là lý do logging.level.org.hibernate.SQL: DEBUG hoạt động dù Hibernate không dùng SLF4J trực tiếp.

Bài tiếp theo: Structured logging (Boot 3.4) + production

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên