Golang HTTP服务器中如何实现带上下文的日志记录

Golang HTTP服务器中如何实现带上下文的日志记录 是否存在一种方法能够记录所有带有上下文的相关日志?比如为该会话/请求分配一个唯一标识符。

  1. HTTP处理函数可能会调用其他通用函数(这些函数也可能被其他处理函数调用),所有这些函数的日志都应包含该唯一标识符,以便关联该会话的所有日志。
8 回复

好的,我会等待您的回复 🙂

更多关于Golang HTTP服务器中如何实现带上下文的日志记录的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


非常感谢。我会想办法解决的,如果找到了会发出来。

抱歉,我们没有使用 mux,在我们的案例中只需要提供两个端点,这种方式是一个合适的解决方案。

你需要检查是否可以在 mux 中使用类似的技术。

我正在使用mux和处理程序进行路由和CORS设置,有没有办法可以将这个记录器传递给这些处理程序?

看起来在这个示例中我们重写了HandleFunc和Handler。

我们工作中有一个HTTP工具正是这样做的。

我记得我们使用 zap 进行日志记录,并通过 With() 方法"派生"一个新的日志记录器,然后将其注入到请求上下文中。

一旦我能够访问VPN,我就会为你提取相关的代码路径。

func main() {
    fmt.Println("hello world")
}

很抱歉让您久等,由于防火墙固件更新,我仍然无法访问公司VPN。如果IT部门在接下来2小时内无法解决这个问题,我就得去办公室上班而不是远程办公了。所以我想大概4小时内或更早就能发布这个问题的解决方案。

// 代码示例保留原样

基本内容如下:

package service

import (
	"net/http"
	"sync"
	"time"

	"go.uber.org/zap"
)

type HandleFunc func(*zap.Logger, http.ResponseWriter, *http.Request)

type Handler struct {
	f HandleFunc
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	rid := requestID()

	logger := zap.L().With(zap.String("request_url", r.RequestURI), zap.Int64("request_id", rid), zap.String("method", r.Method))

	logger.Info("Received request")
	start := time.Now()

	h.f(logger, w, r)

	duration := time.Since(start)
	logger.Info("Finished request", zap.Duration("duration", duration))
}

func NewHandler(f HandleFunc) *Handler {
	return &Handler{f: f}
}

然后我们这样使用它:

http.Handle("/foo", service.NewHandler(service.Foo))
zap.L().Panic("HTTP server crashed", zap.Error(http.ListenAndServe(addr, nil)))

是的,可以通过中间件为每个HTTP请求分配唯一标识符,并使用Go的context包在请求处理链中传递这个标识符,从而实现带上下文的日志记录。以下是一个完整的实现示例:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/google/uuid"
)

// 定义上下文键类型
type contextKey string

const (
    requestIDKey contextKey = "requestID"
)

// 日志记录函数,从上下文中获取请求ID
func logWithContext(ctx context.Context, message string) {
    requestID, ok := ctx.Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    log.Printf("[%s] %s", requestID, message)
}

// 中间件:为每个请求分配唯一ID并注入上下文
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 生成唯一请求ID
        requestID := uuid.New().String()
        
        // 将请求ID添加到响应头(可选)
        w.Header().Set("X-Request-ID", requestID)
        
        // 创建带有请求ID的新上下文
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        
        // 记录请求开始
        logWithContext(ctx, "Request started")
        
        // 使用新上下文继续处理
        start := time.Now()
        next.ServeHTTP(w, r.WithContext(ctx))
        
        // 记录请求完成
        logWithContext(ctx, fmt.Sprintf("Request completed in %v", time.Since(start)))
    })
}

// 通用业务函数示例
func processOrder(ctx context.Context, orderID string) {
    logWithContext(ctx, fmt.Sprintf("Processing order: %s", orderID))
    
    // 模拟业务逻辑
    validateOrder(ctx, orderID)
    updateInventory(ctx, orderID)
}

func validateOrder(ctx context.Context, orderID string) {
    logWithContext(ctx, fmt.Sprintf("Validating order: %s", orderID))
    // 验证逻辑...
}

func updateInventory(ctx context.Context, orderID string) {
    logWithContext(ctx, fmt.Sprintf("Updating inventory for order: %s", orderID))
    // 库存更新逻辑...
}

// HTTP处理函数
func orderHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // 从查询参数获取订单ID(示例)
    orderID := r.URL.Query().Get("order_id")
    if orderID == "" {
        orderID = "default_order"
    }
    
    logWithContext(ctx, "Order handler started")
    
    // 调用通用业务函数
    processOrder(ctx, orderID)
    
    logWithContext(ctx, "Order handler completed")
    w.Write([]byte("Order processed successfully"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/order", orderHandler)
    
    // 包装中间件
    handler := requestIDMiddleware(mux)
    
    log.Println("Starting server on :8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        log.Fatal(err)
    }
}

运行此服务器并访问 http://localhost:8080/order?order_id=123,日志输出将类似:

[550e8400-e29b-41d4-a716-446655440000] Request started
[550e8400-e29b-41d4-a716-446655440000] Order handler started
[550e8400-e29b-41d4-a716-446655440000] Processing order: 123
[550e8400-e29b-41d4-a716-446655440000] Validating order: 123
[550e8400-e29b-41d4-a716-446655440000] Updating inventory for order: 123
[550e8400-e29b-41d4-a716-446655440000] Order handler completed
[550e8400-e29b-41d4-a716-446655440000] Request completed in 102.4µs

关键实现要点:

  1. 中间件模式:使用 requestIDMiddleware 为每个请求生成唯一ID
  2. 上下文传递:通过 r.WithContext(ctx) 将请求ID注入请求上下文
  3. 类型安全:使用自定义 contextKey 类型避免上下文键冲突
  4. 通用日志函数logWithContext 从上下文中提取请求ID并记录
  5. 函数间传递:所有业务函数都接收 context.Context 参数

这种方法确保了同一请求的所有日志都带有相同的请求ID,便于在分布式系统中追踪完整的请求链路。

回到顶部