Trước khi máy bay lăn bánh, phi công đọc to một bảng kiểm: số hiệu, nhiên liệu, đường băng, giờ sẵn sàng. Log khởi động Spring Boot là đúng cái bảng kiểm đó của ứng dụng — nó đọc ra phiên bản, môi trường, cổng, và thời gian lên. Gần như mọi lần "dịch vụ không chịu lên" đều trả lời được bằng vài dòng trong bảng kiểm ấy, nếu bạn chịu đọc. Và có một cái bẫy đúng như trong buồng lái: nếu bảng kiểm lúc nào cũng kêu mười cảnh báo giả, phi công ngừng nghe — rồi bỏ lỡ cảnh báo thật. Đây là thứ bạn nhìn hàng ngày mà ít khi đọc kỹ, nên bài này đi qua từng dòng.

Log thật

  Starting App v1.0 using Java 21.0.11 with PID 101 (/w/demo/target/demo-1.0.jar started by root in /w/demo)
  No active profile set, falling back to 1 default profile: "default"
  Tomcat initialized with port 8080 (http)
  Starting service [Tomcat]
  Starting Servlet engine: [Apache Tomcat/10.1.30]
  Root WebApplicationContext: initialization completed in 611 ms
  Exposing 8 endpoints beneath base path '/actuator'
  Tomcat started on port 8080 (http) with context path '/'
  Started App in 1.138 seconds (process running for 1.36)

Bốn dòng trả lời được hầu hết câu hỏi khi có sự cố.

Starting App v1.0 using Java 21.0.11 with PID 101 — phiên bản ứng dụng, phiên bản Java, và PID. PID là thứ bạn cần cho jstack, jcmd, jmap — bài 88 sê-ri Java.

No active profile set, falling back to "default" — dòng tôi kiểm đầu tiên khi cấu hình "không chịu áp dụng". Nếu bạn mong prod mà thấy default, vấn đề nằm ở cách truyền profile chứ không ở tệp cấu hình.

Tomcat started on port 8080 with context path '/' — cổng thật và context path. Khác với cấu hình bạn nghĩ mình đặt là biết ngay có nguồn nào đang ghi đè, đúng như bài 6.

Started App in 1.138 seconds (process running for 1.36) — hai con số khác nhau: thời gian Spring khởi động, và tổng thời gian tiến trình. Chênh lệch là thời gian JVM khởi động và nạp lớp.

Theo dõi con số thứ nhất theo thời gian. Nó tăng đều nghĩa là bạn đang thêm bean hoặc thêm việc vào @PostConstruct.

Định dạng mỗi dòng

  2026-08-13T04:41:57.766Z  INFO 101 --- [dich-vu-demo] [main] vd.demo.App : Started App in 1.138 seconds
  └─ thời gian            mức  PID     tên ứng dụng   luồng  logger  thông điệp

Tên luồng là cột hữu ích nhất khi gỡ lỗi đồng thời. Thấy [http-nio-8080-exec-3] là biết đang trong một request; thấy [main] là đang khởi động.

Tên ứng dụng đến từ spring.application.name — đặt nó, nhất là khi log của nhiều dịch vụ gom về một chỗ.

Tắt banner

spring:
  main:
    banner-mode: off

Banner ASCII dễ thương nhưng vô dụng trong log sản xuất. Tắt đi.

Bật thông tin thật sự cần

Xem auto-configuration quyết định gì — bài 8:

java -jar app.jar --debug

Xem SQL (bài 27 sẽ nói kỹ):

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE

Xem request HTTP nào khớp handler nào:

logging:
  level:
    org.springframework.web: DEBUG

Cả ba đều rất ồn — bật khi điều tra, tắt sau đó.

Cấu hình log cho sản xuất

logging:
  level:
    root: INFO
    vd.demo: DEBUG
  pattern:
    console: "%d{ISO8601} %-5level [%X{maRequest}] %logger{36} - %msg%n"

Ba nguyên tắc, và chúng lặp lại bài 98 sê-ri Java:

Ghi ra stdout, không ghi tệp. Docker và Kubernetes thu log từ đó.

Dùng JSON trong sản xuất. Spring Boot 3.4 có sẵn:

logging:
  structured:
    format:
      console: ecs

Với bản cũ hơn thì dùng logstash-logback-encoder.

Đưa mã tương quan vào mọi dòng qua MDC — %X{maRequest} ở trên. Bài 17 sẽ dựng filter tạo mã đó.

Log ồn che mất cảnh báo thật

Đây là bài học tôi mang từ chính blog này: khi log khởi động bình thường có hàng chục dòng WARN vô hại, bạn ngừng đọc chúng — và bỏ lỡ dòng WARN thật. Đúng bảng kiểm kêu mười báo động giả.

Hạ mức những logger ồn cụ thể:

logging:
  level:
    org.apache.catalina: WARN
    org.hibernate.engine.jdbc.spi.SqlExceptionHelper: ERROR

Mục tiêu: log khởi động bình thường không có dòng WARN nào. Đạt được rồi thì mỗi WARN đều đáng đọc.

Hai dòng cần biết khi hỏng

Cổng bị chiếm:

  Web server failed to start. Port 8080 was already in use.

Boot in kèm gợi ý. Đổi cổng bằng --server.port=8081.

Thiếu bean:

  Parameter 0 of constructor in vd.demo.DichVu required a bean of type 'vd.demo.Kho'
  that could not be found.

  Action: Consider defining a bean of type 'vd.demo.Kho' in your configuration.

Thông báo lỗi khởi động của Spring Boot rất tốt — chúng có phần Description và Action. Đọc hết trước khi tìm Google.

Nguyên nhân phổ biến nhất của lỗi này: lớp App không ở package gốc, nên @ComponentScan không thấy package chứa bean. Bài 2 đã nói.

Nếu chỉ nhớ một dòng lệnh từ bài này, nhớ dòng rút gọn bảng kiểm xuống ba câu hỏi quan trọng nhất:

java -jar app.jar 2>&1 | grep -E "profile|Tomcat started|Started App"

Ba dòng đó cho bạn: profile nào đang chạy, cổng thật, và thời gian khởi động — cũng là ba câu hỏi đầu tiên khi ai đó nói "dịch vụ không lên".

Mẫu số chung

Mỗi framework web đều có "bảng kiểm tiền-cất-cánh" của riêng nó, và cả bốn sự kiện quan trọng thì giống nhau ở mọi nơi: phiên bản, môi trường, cổng đã gắn, thời gian sẵn sàng.

  • .NET in Now listening on: http://... cùng Hosting environment: Production — đúng dòng "cổng thật" và "profile" của Boot. Rails có log boot kèm môi trường và cổng; Express/Node in ra ở callback listen. Dù viết gì, khi ai đó kêu "dịch vụ không lên", ba câu hỏi đầu vẫn là: lên ở cổng nào, chạy môi trường nào, mất bao lâu để sẵn sàng. Và "thời gian sẵn sàng tăng đều" là tín hiệu cảnh báo ở mọi stack.
  • Hình dạng log cho container đã hội tụ về một kiểu khắp các ngôn ngữ: sự kiện có cấu trúc (JSON) ghi ra stdout, không ghi tệp, gắn một mã tương quan/trace. Đây là nguyên tắc "log là dòng sự kiện" của Twelve-Factor, và slog của Go, pino của Node, structlog của Python, logback-ECS của Java đều cài đặt cùng một ý — để nền tảng (Docker, K8s, hệ thu log) lo phần gom và xoay vòng.

Sợi chỉ chung đáng mang theo là một luật tín-hiệu-trên-nhiễu thuần tuý của vận hành: một bảng kiểm lúc nào cũng hét mười cảnh báo vô hại sẽ dạy bạn phớt lờ nó, và cái cảnh báo thật lọt qua đúng lúc đó. Mục tiêu ở mọi ngôn ngữ là một đường nền sạch — log khởi động bình thường không còn WARN rác — để mỗi dòng WARN xuất hiện đều mang một ý nghĩa. Đây chính là phiên bản vận hành của bài học "triệu chứng không đo mức nghiêm trọng" ở chặng bảo mật: thứ ồn ào nhất thường vô hại, và bạn chỉ nghe được tiếng thì thầm nguy hiểm khi đã dẹp hết tiếng ồn vô nghĩa quanh nó.

Ngày mai: Actuator — và những endpoint không nên mở ra ngoài.