Hướng dẫn sử dụng thư viện Zap để ghi log hiệu suất cao trong Go

Thư viện Zap là một lựa chọn mạnh mẽ để xử lý việc ghi log trong các ứng dụng Go. Nó nổi bật với hiệu suất cao và khả năng ghi log có cấu trúc, rất hữu ích cho việc gỡ lỗi, giám sát và phân tích hệ thống.

1. Các tính năng chính của Zap

  • Ghi log có cấu trúc: Zap ghi log dưới dạng các cặp khóa-giá trị, giúp việc phân tích dữ liệu log trở nên dễ dàng hơn khi sử dụng các công cụ như ELK Stack hoặc Prometheus.
  • Các mức độ log: Hỗ trợ nhiều mức độ log khác nhau như Debug, Info, Warn, Error, DPanic, Panic và Fatal.
  • Đa dạng phương thức xuất log: Có thể xuất log ra console, file, hoặc qua mạng.

2. Cài đặt

Để sử dụng Zap, bạn cần cài đặt thư viện thông qua Go modules:

go get -u go.uber.org/zap
    

3. Sử dụng cơ bản

3.1. Khởi tạo Logger

Zap cung cấp hai cách khởi tạo logger chính, tùy thuộc vào môi trường hoạt động:

  • zap.NewDevelopment(): Thích hợp cho môi trường phát triển, tạo ra các log dễ đọc hơn.
  • zap.NewProduction(): Dành cho môi trường production, tạo ra các log gọn gàng, tối ưu cho việc phân tích bằng máy.
package main

import (
	"log"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func main() {
	// Khởi tạo logger cho môi trường production
	logger, err := zap.NewProduction()
	if err != nil {
		log.Fatalf("Không thể khởi tạo logger: %v", err)
	}
	defer logger.Sync() // Đảm bảo tất cả các log đệm được ghi ra trước khi thoát

	logger.Info("Logger production đã khởi tạo thành công.")

	// Ví dụ sử dụng logger phát triển
	devLogger, _ := zap.NewDevelopment()
	defer devLogger.Sync()
	devLogger.Debug("Đây là log debug từ môi trường phát triển.")
}
    

3.2. Thiết lập mức độ log và định dạng thời gian

Bạn có thể tùy chỉnh mức độ log để chỉ hiển thị các log từ một cấp độ nhất định trở lên (ví dụ: chỉ hiển thị Warn và Error). Đồng thời, có thể định dạng lại cách hiển thị thời gian trong log.

func configureLoggerLevelAndTimestamp() {
	config := zap.NewProductionConfig()
	// Thiết lập mức độ log là WarnLevel, các log Debug và Info sẽ không được xuất ra
	config.Level = zap.NewAtomicLevelAt(zapcore.WarnLevel)
	// Định dạng thời gian theo "YYYY-MM-DD HH:MM:SS"
	config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")

	logger, err := config.Build()
	if err != nil {
		log.Fatalf("Không thể xây dựng logger với cấu hình: %v", err)
	}
	defer logger.Sync()

	logger.Debug("Thông báo gỡ lỗi - Sẽ không hiển thị.")
	logger.Info("Thông tin hệ thống - Sẽ không hiển thị.")
	logger.Warn("Cảnh báo hệ thống",
		zap.String("component", "database"),
		zap.Int("connections", 50),
		zap.Duration("latency", 30*time.Millisecond),
	)
	logger.Error("Lỗi nghiêm trọng", zap.Error(err)) // Giả sử err có giá trị
}
    

3.3. Sử dụng Sugar Logger

Sugar Logger cung cấp một API linh hoạt hơn, cho phép sử dụng các hàm ghi log có định dạng chuỗi tương tự như printf. Cách này có thể làm giảm hiệu suất khoảng 10% so với logger thông thường.

func useSugarLogger() {
	logger, _ := zap.NewDevelopment()
	sugarLogger := logger.Sugar() // Lấy ra Sugar logger
	defer logger.Sync()

	sugarLogger.Infof("Thông tin quan trọng: %d người dùng đang hoạt động.", 1024)
	sugarLogger.Warnln("Cảnh báo:", "Dung lượng đĩa sắp đầy.")
}
    

3.4. Thay thế Logger toàn cục

Bạn có thể thay thế logger mặc định của Zap bằng logger đã được tùy chỉnh của mình. Sau khi thay thế, bạn có thể truy cập logger toàn cục thông qua zap.L() và Sugar logger toàn cục qua zap.S().

func initGlobalLogger() {
	config := zap.NewDevelopmentConfig()
	config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
	logger, _ := config.Build()
	zap.ReplaceGlobals(logger) // Thay thế logger toàn cục
}

func writeToGlobalLogger() {
	zap.L().Info("Ghi log thông qua logger toàn cục.")
	zap.S().Warn("Ghi log Sugar thông qua logger toàn cục.")
}
    

3.5. Ghi log ra nhiều đích

Zap cho phép bạn cấu hình để ghi log đồng thời ra nhiều nơi, ví dụ như console và file log.

import (
	"os"
	"sync"
	"path/filepath"
)

func writeToConsoleAndFile() {
	config := zap.NewDevelopmentConfig()
	config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")

	// Cấu hình ghi ra console
	consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
	consoleSyncer := zapcore.Lock(os.Stdout)
	consoleCore := zapcore.NewCore(consoleEncoder, consoleSyncer, zapcore.InfoLevel)

	// Cấu hình ghi ra file
	logFile, err := os.OpenFile("application.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatalf("Không thể mở file log: %v", err)
	}
	fileEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig) // Có thể dùng JSONEncoder cho production
	fileSyncer := zapcore.AddSync(logFile)
	fileCore := zapcore.NewCore(fileEncoder, fileSyncer, zapcore.InfoLevel)

	// Kết hợp hai core lại
	combinedCore := zapcore.NewTee(consoleCore, fileCore)

	logger := zap.New(combinedCore, zap.AddCaller())
	defer logger.Sync()

	logger.Info("Thông điệp này sẽ xuất hiện trên cả console và file log.")
}
    

3.6. Phân chia file log theo thời gian

Để quản lý dung lượng file log, bạn có thể tùy chỉnh cách ghi log sao cho các file log được tạo mới theo từng khoảng thời gian nhất định (ví dụ: hàng ngày).

// Định nghĩa một writer tùy chỉnh để ghi log theo thời gian
    type timeRoller struct {
        mu sync.Mutex
        currentFile *os.File
        currentDate string
    }

    func (tr *timeRoller) Write(p []byte) (n int, err error) {
        tr.mu.Lock()
        defer tr.mu.Unlock()

        now := time.Now().Format("2006-01-02") // Định dạng ngày
        if tr.currentFile != nil && now == tr.currentDate {
            return tr.currentFile.Write(p) // Nếu cùng ngày, ghi vào file hiện tại
        }

        // Đóng file cũ nếu có
        if tr.currentFile != nil {
            tr.currentFile.Close()
        }

        // Tạo thư mục logs nếu chưa tồn tại
        logDir := "./logs"
        if err := os.MkdirAll(logDir, 0755); err != nil {
            return 0, err
        }

        // Tạo file log mới
        fileName := filepath.Join(logDir, now+".log")
        file, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return 0, err
        }
        tr.currentFile = file
        tr.currentDate = now
        return file.Write(p)
    }

    func splitLogByTime() {
        config := zap.NewDevelopmentConfig()
        config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")

        // Sử dụng MultiWriteSyncer để ghi ra console và custom roller
        roller := &timeRoller{}
        multiSyncer := zapcore.NewMultiWriteSyncer(
            zapcore.AddSync(os.Stdout), // Ghi ra console
            zapcore.AddSync(roller),      // Ghi ra file phân chia theo ngày
        )

        core := zapcore.NewCore(
            zapcore.NewConsoleEncoder(config.EncoderConfig),
            multiSyncer,
            zapcore.InfoLevel,
        )

        logger := zap.New(core, zap.AddCaller())
        defer logger.Sync()

        // Ghi một vài log để kiểm tra
        for i := 0; i < 5; i++ {
            logger.Sugar().Infof("Ghi log lần thứ: %d", i+1)
            time.Sleep(500 * time.Millisecond) // Tạm dừng để quan sát
        }
    }
    

Thẻ: Go zap logging structured logging performance

Đăng vào ngày 5 tháng 11 lúc 21:25