Structured Logging — bản chất, định dạng dữ liệu và nguyên lý hoạt động trong ứng dụng

Tác giả: IT Sectr Đã đăng: 2026-05-28 Thời gian đọc: 8 phút

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 — biểu diễn log ở định dạng khóa-giá trị thay vì văn bản phẳng, phù hợp cho xử lý tự động
  • JSON — định dạng log cấu trúc phổ biến nhất, được hỗ trợ bởi tất cả hệ thống thu thập và phân tích hiện đại
  • Logfmt — định dạng nhỏ gọn từ Heroku, thuận tiện cho đọc người và phân tích bằng grep
  • ELK Stack — Elasticsearch, Logstash, Kibana — cơ sở hạ tầng tiêu chuẩn để lưu trữ và trực quan hóa log cấu trúc
  • Ngữ cảnh — ID yêu cầu, phiên người dùng, phiên bản ứng dụng — các trường bắt buộc của mỗi thông điệp cấu trúc

Structured Logging là gì

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.

Các định dạng log cấu trúc

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ạngVí dụKhi nào sử dụng
JSON{"event":"login","user_id":42}ELK Stack, bộ thu thập đám mây, microservice
Logfmtevent=login user_id=42 duration_ms=150Console, tail, heroku logs
MessagePackTương đương nhị phân của JSONHệ thống tải cao, IoT

JSON — định dạng phổ quát

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 — định dạng nhỏ gọn

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 cutawk. 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.

Tại sao cần Structured Logging trong phát triển di động

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.

Công cụ thu thập và phân tích

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.

swift
// 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
    }
}

Các phương pháp tốt nhất cho Structured Logging

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.

kotlin
// 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)
    }
}

Các trường bắt buộc

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.

Ví dụ ghi log cấu trúc trong Swift và Kotlin

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ủ.

swift
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)")
    }
}

Structured vs Unstructured: so sánh cách tiếp cận

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ảnLog cấu trúc
Khả năng đọcCao trong consoleTrung bình (cần pretty-print)
Tìm kiếmgrep theo chuỗi conTruy vấn theo trường và giá trị
Tổng hợpKhông được hỗ trợTrung bình, trung vị, phân vị
Tích hợpYêu cầu phân tíchTí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

Định dạng nào tốt nhất cho ghi log di động?

Để 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ó cần ghi log ở định dạng cấu trúc trên máy khách khô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 khác JSON như thế nào?

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.

Làm thế nào để thêm correlation ID vào tất cả log?

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ể trộn log cấu trúc và log văn bản không?

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

  • Structured Logging — định dạng log với các cặp khóa-giá trị, phù hợp cho lập chỉ mục và truy vấn tự động, không giống chuỗi văn bản
  • JSON và logfmt — các định dạng chính: JSON phổ quát cho hệ thống thu thập, logfmt nhỏ gọn cho xem console và log docker
  • Correlation ID — trường bắt buộc của mỗi thông điệp cấu trúc, không có nó không thể liên kết log thành phiên người dùng
  • ELK Stack và Grafana Loki — các giải pháp cơ sở hạ tầng tiêu chuẩn để lưu trữ, lập chỉ mục và trực quan hóa log cấu trúc
  • Hiệu suất — các nhóm với structured logging phát hiện sự cố nhanh hơn 4 lần nhờ truy vấn theo trường thay vì grep theo văn bản
  • Phân loại — số nên được truyền dưới dạng số, không phải chuỗi, để cho phép tổng hợp (trung bình, trung vị, phân vị) trong hệ thống phân tích

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.

Thảo luận dự án

Đọc thêm