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:
-
Tự động sinh
RequestIDcho mỗi lượt truy cập. -
Tự động đo lường thời gian xử lý API.
-
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.
-
Truyền
RequestIDxuyên suốt các tầng nghiệp vụ bằngcontext.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:
-
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. -
Value của
"password"đã tự động biến thành"***MASKED***". -
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