0

Bài 6: Tích hợp slog với context.Context (Tracking luồng Request)

\Trong hệ thống phân tán (Microservices) hoặc các ứng dụng Backend lớn, một Request từ người dùng thường đi qua rất nhiều tầng: Middleware -> Handler -> UseCase -> Repository -> gọi API bên thứ ba. Nếu có lỗi xảy ra ở tầng Repository, dòng log ghi lại lỗi đó đứng trơ trọi một mình. Bạn không thể biết luồng xử lý đó thuộc về ai, gọi từ endpoint nào.

Giải pháp cho bài toán này là Distributed Tracing (Truy vết phân tán). Ý tưởng cốt lõi: Ngay khi Request chạm vào Server, ta sinh ra một mã duy nhất (gọi là TraceID hoặc RequestID). Mã này được "nhét" vào context.Context, truyền đi khắp mọi hàm. Cuối cùng, mọi thao tác ghi log sẽ tự động móc TraceID này ra và đính kèm vào JSON. Nhờ đó, bạn chỉ cần search một mã trên Elasticsearch là sẽ thấy toàn bộ hành trình của Request đó từ lúc sinh ra đến lúc kết thúc.

1. Vấn đề của Logger kế thừa (logger.With)

Ở Bài 4, chúng ta đã học cách gắn thuộc tính bằng logger.With(). Tuy nhiên, cách này bắt bạn phải truyền tay đối tượng logger đi khắp mọi nơi trong mã nguồn, biến mọi hàm (từ logic nghiệp vụ đến query DB) thành những hàm phụ thuộc vào bộ log. Đây là một sự vi phạm nghiêm trọng kiến trúc sạch (Clean Architecture).

Chuẩn mực của Go là truyền context.Context làm tham số đầu tiên ở mọi hàm nghiệp vụ. Do đó, slog tận dụng luôn Context để mang dữ liệu log đi theo, vừa đúng chuẩn, vừa không làm bẩn logic.

2. Xây dựng Context-Aware Handler

Để slog tự động lấy dữ liệu từ Context, chúng ta phải tự định nghĩa một Custom Handler bằng cách bọc (wrap) lại một Handler tiêu chuẩn (như JSONHandler).

Go

package main

import (
	"context"
	"log/slog"
	"os"
)

// 1. Định nghĩa kiểu khóa riêng cho Context để tránh xung đột
type contextKey string
const traceIDKey contextKey = "trace_id"

// 2. Định nghĩa Custom Handler
type ContextHandler struct {
	// Kế thừa interface slog.Handler. 
	// Thực tế bên trong nó sẽ chứa một slog.JSONHandler
	slog.Handler 
}

// 3. Ghi đè hàm Handle - Trái tim của quá trình ghi log
// Hàm này được tự động gọi mỗi khi có lệnh slog.Info, slog.Error,...
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	
	// Trích xuất TraceID từ context của luồng hiện tại
	if traceID, ok := ctx.Value(traceIDKey).(string); ok {
		// Thêm cặp Key-Value mới trực tiếp vào dữ liệu Record trước khi in
		r.AddAttrs(slog.String("trace_id", traceID))
	}

	// Đẩy Record (đã được thêm trace_id) vào Handler lõi (JSONHandler) để xuất ra chuỗi json
	return h.Handler.Handle(ctx, r)
}

3. Tích hợp Handler mới vào vòng đời ứng dụng

Tiếp theo, ta thay thế cấu hình toàn cục để hệ thống chuyển sang dùng Custom Handler này. Hàm nghiệp vụ (doBusinessLogic) giờ đây không hề biết đến sự tồn tại của bộ log, nó chỉ nhận đúng một tham số là context.Context.

Go

func init() {
	// Tạo JSON Handler chuẩn của thư viện
	baseHandler := slog.NewJSONHandler(os.Stdout, nil)

	// Bọc JSON Handler vào ContextHandler do ta tự chế
	customHandler := &ContextHandler{Handler: baseHandler}

	// Đặt thành mặc định
	slog.SetDefault(slog.New(customHandler))
}

// Hàm nghiệp vụ mô phỏng ở tầng sâu bên trong hệ thống
func doBusinessLogic(ctx context.Context) {
	// Chú ý cách gọi: Dùng hàm có chữ *Context (như InfoContext, ErrorContext) 
	// và BẮT BUỘC TRUYỀN tham số ctx vào. 
	// Nếu gọi slog.Info("...") như cũ, context sẽ rỗng và handler không lấy được trace_id.
	
	slog.InfoContext(ctx, "Đang xử lý nghiệp vụ sâu bên trong")
	
	// Gọi thêm một DB query (mô phỏng)
	slog.WarnContext(ctx, "Database phản hồi chậm")
}

func main() {
	// 1. Mô phỏng khi Request bắt đầu chạm vào Server
	// Sinh ra một ID duy nhất (thực tế hay dùng package uuid)
	reqTraceID := "TRC-777-888" 
	
	// Nhét ID vào một context mới
	ctx := context.WithValue(context.Background(), traceIDKey, reqTraceID)

	// 2. Ghi log tại tầng ngoài cùng
	slog.InfoContext(ctx, "Nhận request mới", slog.String("method", "POST"))

	// 3. Truyền context đi vào các tầng sâu hơn
	doBusinessLogic(ctx)
}

Kết quả JSON trả ra (hãy chú ý mọi dòng log dù ở tầng nào cũng đều có trace_id "TRC-777-888"):

JSON

{"time":"2026-09-18T21:22:15Z","level":"INFO","msg":"Nhận request mới","method":"POST","trace_id":"TRC-777-888"}
{"time":"2026-09-18T21:22:15Z","level":"INFO","msg":"Đang xử lý nghiệp vụ sâu bên trong","trace_id":"TRC-777-888"}
{"time":"2026-09-18T21:22:15Z","level":"WARN","msg":"Database phản hồi chậm","trace_id":"TRC-777-888"}

4. Lưu ý khi Implement Custom Handler

Khi viết Custom Handler bọc (ContextHandler), có hai lỗi kỹ thuật mà lập trình viên rất hay mắc phải nếu không đọc kỹ tài liệu của Go:

  1. Phải cài đặt cả hàm WithAttrs và WithGroup: Interface slog.Handler có 4 hàm (Enabled, Handle, WithAttrs, WithGroup). Nếu bạn chỉ embed và không chịu ghi đè (override) hai hàm With, thì khi ai đó gọi lệnh logger.With("module", "db"), hệ thống sẽ trả về một JSONHandler thuần túy chứ không phải ContextHandler của bạn. Từ đó, chuỗi logic trích xuất Context bị đứt gãy.

  2. Khuyên dùng cấu trúc an toàn:

Go

// Các hàm bắt buộc phải ghi đè để duy trì "vòng kim cô" ContextHandler
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)}
}

Đây là đỉnh cao của việc ứng dụng Structured Logging trong kiến trúc phân tán. Ở bài cuối cùng, chúng ta sẽ lắp ráp hai series lớn nhất lại với nhau: Nhúng hệ thống slog chạy bằng Context này vào một HTTP Server của net/http mà bạn đã làm ở khóa trước.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.