0

Bài 7: Thực hành tổng hợp - Xây dựng HTTP Logging Middleware với slog

Chào mừng bạn đến với bài cuối cùng của series về Logging! Chúng ta sẽ kết hợp những kiến thức tinh túy nhất từ hai package cốt lõi là net/http và log/slog.

Mục tiêu của bài học này là tạo ra một cấu trúc Backend Server chuẩn mực cho Production:

  1. Tự động sinh RequestID cho mỗi lượt truy cập.

  2. Tự động đo lường thời gian xử lý API.

  3. Ghi log chuẩn JSON, có đầy đủ metadata (Method, Path, IP, Status Code) và che giấu các tham số nhạy cảm.

  4. Truyền RequestID xuyên suốt các tầng nghiệp vụ bằng context.Context.

1. Chuẩn bị cơ sở hạ tầng (Custom Handler & Context)

Đầu tiên, chúng ta bê nguyên cấu trúc ContextHandler từ Bài 6, kết hợp với bộ lọc từ khóa nhạy cảm (như "password") từ Bài 5.

Go

package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"time"

	// Go 1.22+ khuyên dùng package rand v2 cho việc sinh số ngẫu nhiên
	"math/rand/v2" 
)

// --- 1. CONFIG: CUSTOM HANDLER & MASKING ---

type contextKey string
const reqIDKey contextKey = "request_id"

var sensitiveKeys = []string{"password", "token"}

func isSensitive(key string) bool {
	lowerKey := strings.ToLower(key)
	for _, k := range sensitiveKeys {
		if strings.Contains(lowerKey, k) {
			return true
		}
	}
	return false
}

type ContextHandler struct {
	slog.Handler
}

// Bóc tách Request ID từ Context và chèn vào log
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	if reqID, ok := ctx.Value(reqIDKey).(string); ok {
		r.AddAttrs(slog.String("request_id", reqID))
	}
	return h.Handler.Handle(ctx, r)
}

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return &ContextHandler{Handler: h.Handler.WithAttrs(attrs)}
}
func (h *ContextHandler) WithGroup(name string) slog.Handler {
	return &ContextHandler{Handler: h.Handler.WithGroup(name)}
}

// Hàm khởi tạo bộ Logger toàn cục
func initLogger() {
	opts := &slog.HandlerOptions{
		// Ẩn dữ liệu nhạy cảm
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if isSensitive(a.Key) {
				return slog.String(a.Key, "***MASKED***")
			}
			return a
		},
	}
	
	jsonHandler := slog.NewJSONHandler(os.Stdout, opts)
	customHandler := &ContextHandler{Handler: jsonHandler}
	slog.SetDefault(slog.New(customHandler))
}

2. Xây dựng Trạm gác: Cấu trúc ResponseWriter giả (ResponseRecorder)

Có một giới hạn rất lớn của đối tượng http.ResponseWriter mặc định trong Go: Nó chỉ cho phép bạn ghi Status Code gửi đi (như 200 hay 404), nhưng không cho phép bạn đọc lại giá trị đó.

Nếu Middleware muốn ghi log xem API trả về mã lỗi bao nhiêu, nó bắt buộc phải tự "chế" ra một đối tượng bọc ngoài ResponseWriter để lưu lại Status Code ngay khi Handler chính thực thi lệnh .WriteHeader().

Go

// --- 2. RESPONSE RECORDER ---

// responseRecorder bọc http.ResponseWriter để "nghe lén" Status Code
type responseRecorder struct {
	http.ResponseWriter
	statusCode int
}

// Ghi đè hàm WriteHeader mặc định
func (rec *responseRecorder) WriteHeader(code int) {
	// Ghi nhớ lại mã code
	rec.statusCode = code 
	// Sau đó mới gọi hàm thực tế để trả về cho Client
	rec.ResponseWriter.WriteHeader(code) 
}

3. Xây dựng Slog Middleware hoàn chỉnh

Đây là tâm điểm của hệ thống. Nó thực hiện 3 việc: Khởi tạo Request ID, đo giờ, và ghi một dòng Log báo cáo tóm tắt toàn bộ vòng đời của API đó.

Go

// --- 3. MIDDLEWARE CHÍNH ---

func SlogMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// 1. Sinh Request ID ảo (thực tế nên dùng UUID library)
		reqID := fmt.Sprintf("REQ-%d", rand.IntN(999999))

		// 2. Nhét Request ID vào Context mới
		ctx := context.WithValue(r.Context(), reqIDKey, reqID)
		
		// Thay thế context cũ của Request bằng context vừa tạo
		r = r.WithContext(ctx)

		// 3. Khởi tạo ResponseRecorder mặc định là 200 OK
		rec := &responseRecorder{
			ResponseWriter: w,
			statusCode:     http.StatusOK, 
		}

		// 4. Chuyển tiếp cho các logic bên trong xử lý 
		// (truyền rec thay vì w, và r đã có context mới)
		next.ServeHTTP(rec, r)

		// 5. GHI LOG (Thực thi sau khi Request đã chạy xong)
		// Lấy IP của client (r.RemoteAddr)
		duration := time.Since(start)

		// In ra JSON log
		slog.InfoContext(r.Context(), "API Request Completed",
			slog.String("method", r.Method),
			slog.String("path", r.URL.Path),
			slog.Int("status", rec.statusCode),
			slog.String("ip", r.RemoteAddr),
			slog.Duration("latency", duration), // Tự động ghi thời gian miligiây
		)
	})
}

4. Kết nối Logic nghiệp vụ và Chạy Server

Bây giờ ta sẽ viết các Handler xử lý logic, truyền Context vào sâu bên trong và khởi động Server bằng Go 1.22 routing.

Go

// --- 4. BUSINESS LOGIC & HANDLERS ---

// Hàm xử lý sâu bên trong logic (chỉ nhận ctx làm tham số)
func processPayment(ctx context.Context, userID int, pass string) error {
	// Ghi log xử lý. Lưu ý có gắn "password" để test chức năng Masking.
	// BẮT BUỘC dùng InfoContext để log nhận được Request ID
	slog.InfoContext(ctx, "Đang gọi ZaloPay API", 
		slog.Int("user_id", userID), 
		slog.String("password", pass),
	)
	
	// Giả lập thời gian chạy 50 mili-giây
	time.Sleep(50 * time.Millisecond)
	return nil
}

func checkoutHandler(w http.ResponseWriter, r *http.Request) {
	// Trích xuất context đã mang theo Request ID từ Middleware
	ctx := r.Context()

	// Gọi hàm nghiệp vụ và truyền context đi theo
	err := processPayment(ctx, 1024, "my_secret_pass_123")
	
	if err != nil {
		slog.ErrorContext(ctx, "Thanh toán lỗi")
		http.Error(w, "Lỗi server", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated) // Báo mã 201 Created
	w.Write([]byte("Thanh toán thành công"))
}

// --- 5. HÀM MAIN ---

func main() {
	// Cấu hình Logger
	initLogger()
	slog.Info("Server đang khởi động", "port", 8080)

	// Go 1.22 Routing
	mux := http.NewServeMux()
	mux.HandleFunc("POST /api/checkout", checkoutHandler)

	// Bọc toàn bộ ứng dụng bằng Slog Middleware
	handler := SlogMiddleware(mux)

	// Khởi động server
	if err := http.ListenAndServe(":8080", handler); err != nil {
		slog.Error("Lỗi khởi động Server", "chi_tiet", err)
	}
}

5. Kết quả đỉnh cao

Hãy gửi một lệnh test bằng curl ở Terminal khác: curl -X POST http://localhost:8080/api/checkout

Bạn sẽ thấy hai dòng log in ra. Một dòng từ hàm processPayment ở tít sâu bên trong, và một dòng tổng kết (summary) do Middleware ghi lại ở ngoài cùng.

Điểm ăn tiền nằm ở đây:

  1. Cả hai dòng đều có chung "request_id":"REQ-...". Nếu hệ thống có lỗi, bạn copy mã này paste vào ô tìm kiếm của Elasticsearch là thấy toàn bộ mạch thời gian.

  2. Value của "password" đã tự động biến thành "***MASKED***".

  3. Có đẩy đủ status (201) và tốc độ phản hồi (latency).

Kết quả JSON thực tế:

JSON

{"time":"2026-09-18T21:25:10Z","level":"INFO","msg":"Server đang khởi động","port":8080}
{"time":"2026-09-18T21:25:21Z","level":"INFO","msg":"Đang gọi ZaloPay API","user_id":1024,"password":"***MASKED***","request_id":"REQ-481923"}
{"time":"2026-09-18T21:25:21Z","level":"INFO","msg":"API Request Completed","method":"POST","path":"/api/checkout","status":201,"ip":"127.0.0.1:54321","latency":51234500,"request_id":"REQ-481923"}

Vậy là chúng ta đã kết thúc series về log và Structured Logging slog trong Go. Cùng với series net/http, Hiếu đã sở hữu một bộ khung (scaffolding) vô cùng cứng cáp, đáp ứng đầy đủ tiêu chuẩn bảo mật, hiệu năng và dễ dàng tracing cho bất kỳ dự án API hạng nặng nào.


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í