Structured Logging là một cách tiếp cận ghi log trong đó mỗi thông điệp được biểu diễn ở định dạng máy có thể đọc được với các cặp khóa-giá trị, thay vì dạng văn bản không cấu trúc. Không giống như chuỗi phẳng, log cấu trúc chứa siêu dữ liệu: dấu thời gian, mức, mô-đun, ID yêu cầu — và có thể được lập chỉ mục bởi các hệ thống phân tích. Theo O'Reilly Effective Logging, chuyển sang định dạng cấu trúc giúp giảm thời gian tìm kiếm sự cố từ hàng giờ xuống còn vài phút nhờ khả năng lọc theo trường. Đây là tiêu chuẩn thực tế trong phát triển di động và máy chủ hiện đại: JSON và logfmt cho phép xử lý log bằng chương trình, không phải bằng mắt.
Điểm chính
Structured Logging là một phương pháp ghi log trong đó mỗi thông điệp chứa các trường được đặt tên với các giá trị được phân loại. Thay vì một chuỗi như User 42 logged in from device ABC, một log cấu trúc trông như một tập hợp các trường: user_id=42, event=login, device_id=ABC, timestamp=2026-07-04T10:30:00Z.
Ưu điểm chính của log cấu trúc so với log văn bản là khả năng xử lý bằng chương trình. Phân tích log văn bản đòi hỏi biểu thức chính quy và các giả định về định dạng chuỗi. Log cấu trúc được phân tích không mất mát: mỗi trường có kiểu và tên được biết trước, cho phép xây dựng các truy vấn như tìm tất cả lỗi xác thực trong giờ qua cho người dùng 42 mà không cần xử lý thêm.
Theo Honeycomb.io (2023), các nhóm sử dụng structured logging trong sản xuất phát hiện sự cố nhanh hơn trung bình 4 lần so với các nhóm dựa vào log văn bản và grep.
Structured Logging hỗ trợ nhiều định dạng tuần tự hóa. Việc chọn định dạng phụ thuộc vào cơ sở hạ tầng: JSON thuận tiện cho tích hợp với Elasticsearch và hệ thống đám mây, logfmt cho xem console qua tail và grep, Protocol Buffers cho hệ thống hiệu suất cao với giới hạn băng thông.
| Định dạng | Ví dụ | Khi nào sử dụng |
|---|---|---|
| JSON | {"event":"login","user_id":42} | ELK Stack, bộ thu thập đám mây, microservice |
| Logfmt | event=login user_id=42 duration_ms=150 | Console, tail, heroku logs |
| MessagePack | Tương đương nhị phân của JSON | Hệ thống tải cao, IoT |
JSON là định dạng phổ biến nhất cho log cấu trúc. Nó được hỗ trợ sẵn bởi tất cả hệ thống thu thập: Logstash, Fluentd, Amazon CloudWatch, Google Cloud Logging. Log JSON dễ đọc bởi con người và phân tích bởi bất kỳ ngôn ngữ lập trình nào mà không cần thư viện bổ sung. Nhược điểm chính là dài dòng: mỗi cặp khóa-giá trị yêu cầu dấu ngoặc kép và dấu hai chấm, làm tăng khối lượng dữ liệu lưu trữ lên 30–50% so với logfmt.
Logfmt được phát triển tại Heroku để hiển thị console. Nó nhỏ gọn hơn JSON, giữ được tính dễ đọc cho con người và có thể dễ dàng xử lý với cut và awk. Ví dụ: ts=2026-07-04T10:30:00Z level=error module=api status=500. Logfmt không yêu cầu thoát hầu hết các ký tự và phù hợp cho ghi log stdout trong container.
Trong các ứng dụng di động, Structured Logging giải quyết ba vấn đề chính: tìm nguyên nhân sự cố mà không cần tái tạo trên thiết bị, theo dõi phiên người dùng và phân tích hiệu suất theo phiên bản ứng dụng.
Log văn bản trên thiết bị di động hầu như vô dụng — nhà phát triển không thể grep log trên thiết bị của người dùng. Log cấu trúc được gửi đến các hệ thống đám mây (Firebase, Sentry, Datadog) và được lập chỉ mục ở đó. Bạn có thể xây dựng truy vấn như hiển thị tất cả sự cố trên iOS 17.4, phiên bản ứng dụng 3.2, trong mô-đun checkout và nhận được lựa chọn chính xác trong vài giây.
Theo Sentry (2024), các ứng dụng sử dụng breadcrumbs cấu trúc có nhiều ngữ cảnh hơn 60% trong mỗi báo cáo sự cố so với các ứng dụng chỉ ghi log văn bản lỗi. Điều này ảnh hưởng trực tiếp đến tốc độ sửa lỗi.
ELK Stack — Elasticsearch, Logstash, Kibana — vẫn là cơ sở hạ tầng tiêu chuẩn để làm việc với log cấu trúc. Logstash nhận log ở định dạng JSON, chuyển đổi và gửi đến Elasticsearch để lập chỉ mục, Kibana cung cấp giao diện trực quan cho các truy vấn và bảng điều khiển.
Cho ứng dụng di động, các giải pháp đám mây phổ biến: Firebase Crashlytics với log tùy chỉnh, Sentry với breadcrumbs, Datadog với theo dõi APM. Chúng chấp nhận log cấu trúc trực tiếp từ SDK di động và không yêu cầu triển khai backend riêng. Firebase cung cấp gói miễn phí cho báo cáo sự cố, Senty bổ sung theo dõi phân tán, và Datadog tích hợp với APM để theo dõi hiệu suất yêu cầu trên cả máy khách và máy chủ đồng thời.
Grafana Loki — một thay thế cho Elasticsearch được tối ưu hóa cho log. Loki không lập chỉ mục nội dung thông điệp theo mặc định mà sử dụng nhãn (labels) để lọc. Điều này rẻ hơn đáng kể trong lưu trữ và nhanh hơn cho các truy vấn trên một tập trường cố định.
// Ghi log cấu trúc qua Swift Logger trong JSON
struct StructuredLog {
let event: String
let attributes: [String: Any]
let level: String
func serialize() -> String {
var base = "event=\(event) level=\(level)"
for (key, value) in attributes {
base += " \(key)=\(value)"
}
return base
}
}
Quy tắc đầu tiên của ghi log cấu trúc: mỗi thông điệp phải chứa một định danh yêu cầu hoặc phiên. Không có ngữ cảnh, một log riêng lẻ sẽ vô dụng — không thể xác định nó thuộc về người dùng hay yêu cầu nào. Thêm correlation ID khi bắt đầu phiên và truyền nó qua tất cả các lớp của ứng dụng.
Quy tắc thứ hai: phân loại trường. Các trường số (duration_ms, status_code, retry_count) nên được truyền dưới dạng số, không phải chuỗi. Elasticsearch và các hệ thống tương tự lập chỉ mục số và chuỗi khác nhau: số có thể được tổng hợp (trung bình, trung vị, phân vị), chuỗi hỗ trợ tìm kiếm toàn văn. Phân loại sai sẽ loại bỏ khả năng xây dựng bảng điều khiển phân tích.
Quy tắc thứ ba: tránh các đối tượng lồng nhau. Log JSON với độ sâu lồng hơn 2 cấp rất khó để lọc và trực quan hóa. Thay vì {"user": {"name": "Alice", "role": "admin"}}, hãy sử dụng khóa phẳng: user_name=Alice user_role=admin.
// Ghi log cấu trúc trên Android qua Timber + logfmt
class StructuredTree : Timber.Tree() {
override fun log(priority: Int, tag: String?,
message: String?, t: Throwable?) {
val level = priorityToLevel(priority)
val logfmt = "level=$level tag=$tag message=$message"
sendToRemote(logfmt)
}
}
Tập trường tối thiểu cho mỗi thông điệp cấu trúc: timestamp ở định dạng ISO 8601, level (debug/info/warn/error/fatal), logger (tên mô-đun hoặc lớp), message (mô tả sự kiện có thể đọc được). Bổ sung: correlation_id, user_id (nếu biết), version (phiên bản ứng dụng), platform (iOS/Android), environment (dev/staging/prod).
Không có correlation_id, log cấu trúc trở thành một tập hợp các bản ghi rời rạc không thể liên kết thành một kịch bản người dùng duy nhất. Tạo UUID khi mỗi lần khởi chạy ứng dụng và thêm nó vào tất cả log phiên. Trong thực tế, correlation_id nên được truyền qua tất cả các lớp: từ sự kiện UI đến yêu cầu mạng và tác vụ nền — nếu không, một số log sẽ không có ngữ cảnh và không tham gia phân tích. Với theo dõi đầu-cuối, một UUID duy nhất cho phép thu thập bức tranh toàn cảnh về hành trình người dùng.
Trên iOS, ghi log cấu trúc có thể được thực hiện thông qua một lớp bọc trên os_log giúp tuần tự hóa các trường thành định dạng logfmt. Trên Android, thông qua Timber với một Tree tùy chỉnh chuyển đổi thông điệp thành JSON hoặc logfmt trước khi gửi đến máy chủ.
import OSLog
struct StructuredLogger {
let subsystem: String
let category: String
func log(level: OSLogType,
event: String,
context: [String: Any]) {
let oslogger = Logger(
subsystem: subsystem,
category: category
)
let fields = context.map {
"\($0.key)=\($0.value)"
}.joined(separator: " ")
oslogger.log(level: level,
"\(event) \(fields)")
}
}
Sự lựa chọn giữa log cấu trúc và log văn bản phụ thuộc vào giai đoạn dự án. Trong giai đoạn phát triển đầu, log văn bản đơn giản và nhanh hơn — nhà phát triển viết thông điệp trực tiếp không cần lớp bọc thêm. Nhưng một khi dự án vượt quá phạm vi một nhóm hoặc một máy chủ, log cấu trúc trở nên bắt buộc.
| Tiêu chí | Log văn bản | Log cấu trúc |
|---|---|---|
| Khả năng đọc | Cao trong console | Trung bình (cần pretty-print) |
| Tìm kiếm | grep theo chuỗi con | Truy vấn theo trường và giá trị |
| Tổng hợp | Không được hỗ trợ | Trung bình, trung vị, phân vị |
| Tích hợp | Yêu cầu phân tích | Tích hợp sẵn trong ELK/Loki/Datadog |
| Khối lượng lưu trữ | Nhỏ hơn (không siêu dữ liệu) | Lớn hơn (trường + giá trị) |
Câu hỏi thường gặp
Để gửi lên máy chủ, hãy sử dụng JSON — nó được hỗ trợ sẵn bởi Firebase Crashlytics, Sentry và Datadog. Để xem cục bộ trong log Xcode hoặc Android Studio, hãy sử dụng logfmt — nó nhỏ gọn hơn và có thể đọc được mà không cần định dạng.
Có, log cấu trúc trên máy khách cho phép thêm ngữ cảnh vào mỗi báo cáo sự cố: phiên bản HĐH, trạng thái mạng, hành động cuối cùng của người dùng. Không có breadcrumbs cấu trúc, báo cáo sự cố chỉ chứa ngăn xếp cuộc gọi mà không có kịch bản người dùng.
Logfmt nhỏ gọn hơn (giảm 30–50% khối lượng) và dễ đọc hơn trong terminal. JSON hỗ trợ các đối tượng và mảng lồng nhau nhưng yêu cầu thoát dấu ngoặc kép. Lựa chọn phụ thuộc vào cơ sở hạ tầng: cho ELK — JSON, cho xem console — logfmt.
Tạo một thể hiện duy nhất của UUID khi khởi chạy ứng dụng, lưu nó trong singleton hoặc vùng chứa DI và truyền nó cho tất cả logger qua hàm tạo. Thay thế — sử dụng bộ nhớ cục bộ luồng hoặc Continuation Local Storage trong coroutine Kotlin.
Có thể, nhưng không được khuyến nghị — trộn sẽ làm mất khả năng lập chỉ mục tự động. Nếu một số log dựa trên văn bản, chúng phải được phân tích bằng biểu thức chính quy, làm giảm hiệu suất và độ tin cậy của tìm kiếm. Tốt hơn là di chuyển tất cả log sang định dạng cấu trúc.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm