0

Bài 2: Tùy biến Logger tiêu chuẩn

Ở Bài 1, chúng ta đã dùng logger mặc định của Go. Mặc dù tiện lợi, nhưng trên môi trường thực tế, định dạng mặc định (chỉ có Ngày và Giờ) là không đủ. Khi một lỗi xảy ra giữa hàng ngàn dòng code, câu hỏi đầu tiên của kỹ sư backend luôn là: "Dòng log này được in ra từ file nào, ở dòng thứ mấy?".

Ngoài ra, log thực tế cần được lưu xuống file thay vì chỉ in ra màn hình Terminal. Package log cung cấp các công cụ rất linh hoạt để giải quyết những vấn đề này.

1. Thêm thông tin bối cảnh với Flags

Hàm log.SetFlags() cho phép bạn định nghĩa các thành phần sẽ xuất hiện ở đầu mỗi dòng log. Các cờ (Flags) này là các hằng số nguyên, bạn có thể kết hợp chúng lại với nhau bằng toán tử OR bitwise (|).

Các Flags thông dụng nhất:

  • log.Ldate: Ngày hiện tại ở format địa phương (vd: 2026/09/18).

  • log.Ltime: Giờ, phút, giây (vd: 21:18:41).

  • log.Lmicroseconds: Thêm độ phân giải micro-giây (hữu ích khi debug race condition).

  • log.Llongfile: Đường dẫn tuyệt đối tới file code và số dòng (vd: /Users/hieu/project/main.go:25).

  • log.Lshortfile: Chỉ in tên file và số dòng (vd: main.go:25). Khuyên dùng vì gọn gàng hơn.

  • log.LstdFlags: Đây là giá trị mặc định, tương đương với log.Ldate | log.Ltime.

Go

package main

import (
	"log"
)

func main() {
	// Ghi đè cấu hình mặc định: Hiển thị Ngày, Giờ (đến micro-giây) và Tên file + Dòng code
	log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)

	log.Println("Kết nối đến Database thất bại")
}

Kết quả:

Plaintext

2026/09/18 21:18:41.123456 main.go:11: Kết nối đến Database thất bại

2. Phân loại bằng Prefix (Tiền tố)

Hàm log.SetPrefix() giúp bạn chèn một đoạn text cố định vào ngay trước dòng log. Kỹ thuật này thường được dùng để đánh dấu luồng xử lý hoặc mức độ quan trọng.

Go

// Đặt tiền tố báo lỗi
log.SetPrefix("[DATABASE ERROR] ")
log.Println("Timeout connection") 
// Kết quả: [DATABASE ERROR] 2026/09/18 21:18:41 main.go:15: Timeout connection

3. Thay đổi đích đến (Output) xuống File

Theo mặc định, package log ghi dữ liệu ra os.Stderr (luồng lỗi tiêu chuẩn trên Terminal). Để lưu trữ log lâu dài, bạn cần điều hướng Output này xuống một file trên ổ cứng bằng log.SetOutput().

Go

package main

import (
	"log"
	"os"
)

func main() {
	// Mở file (hoặc tạo mới nếu chưa có), ghi nối thêm (Append), cấp quyền đọc/ghi
	file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal("Không thể mở file log:", err)
	}
	defer file.Close() // Nhớ đóng file khi chương trình kết thúc

	// Chuyển hướng log từ Terminal sang File
	log.SetOutput(file)

	log.Println("Dòng log này sẽ nằm im trong file app.log")
}

4. Ghi log song song: Terminal và File (io.MultiWriter)

Khi phát triển local, bạn vừa muốn xem log chạy trên Terminal để dễ nhìn, vừa muốn nó lưu xuống file để đối chiếu. Hàm io.MultiWriter là chìa khóa cho bài toán này. Nó đóng vai trò như một "ống chẻ", nhân bản dữ liệu ghi vào nó ra nhiều đích đến cùng lúc.

Go

package main

import (
	"io"
	"log"
	"os"
)

func main() {
	file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	defer file.Close()

	// Khởi tạo MultiWriter: Kết hợp file và màn hình (os.Stdout)
	multiWriter := io.MultiWriter(os.Stdout, file)

	// Gán vào logger
	log.SetOutput(multiWriter)
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)

	log.Println("Sự kiện này sẽ xuất hiện ở cả màn hình đen và trong file app.log!")
}

5. Khởi tạo Custom Logger bằng log.New (Best Practice)

Từ đầu đến giờ, chúng ta đang dùng các hàm như log.SetFlags, log.SetOutput. Chúng thay đổi cấu hình của Logger Toàn cục (Global Logger). Hạn chế của việc này là toàn bộ chương trình chỉ có một kiểu định dạng duy nhất. Bạn không thể có luồng log báo lỗi (ERROR) xuất ra một file riêng và luồng thông tin (INFO) xuất ra file khác.

Trong thực tế, các dự án sẽ sử dụng hàm log.New() để tạo ra các đối tượng Logger hoàn toàn độc lập với nhau.

Go

package main

import (
	"log"
	"os"
)

var (
	// Khai báo các logger ở tầm vực biến toàn cục để dùng chung
	InfoLog  *log.Logger
	ErrorLog *log.Logger
)

func init() { // Hàm init tự chạy đầu tiên khi khởi động app
	file, err := os.OpenFile("errors.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal(err)
	}

	// Tạo InfoLog: Ghi ra Terminal, có prefix INFO
	InfoLog = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime)

	// Tạo ErrorLog: Ghi ra File, có prefix ERROR và hiện rõ vị trí file
	ErrorLog = log.New(file, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile)
}

func main() {
	InfoLog.Println("Server khởi động tại port 8080")
	// Giả lập một lỗi
	ErrorLog.Println("Không thể kết nối RabbitMQ")
}

Với cấu trúc này, mã nguồn của bạn đã gọn gàng và phân tách rõ ràng hơn rất nhiều. Tuy nhiên, khi hệ thống phình to, kiểu log dạng văn bản (Plain Text) như thế này bộc lộ điểm yếu "chí mạng" khi chúng ta cần tìm kiếm dữ liệu tự động. Đó là lúc chuẩn mực Structured Logging ra đời.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí