Golang Web服务中如何实现带附加字段的日志记录

Golang Web服务中如何实现带附加字段的日志记录 你好 😊 我一直在思考一个关于日志记录的问题,想看看是否有人能帮我解答:

假设我正在编写一个包含许多端点的新 gRPC 服务。我逐渐喜欢上一种模式,即将核心逻辑与 gRPC 相关代码隔离开来。这样,gRPC 处理程序会非常简单,只负责调用执行主要工作的其他函数/方法。我喜欢这种风格,因为大部分代码都独立于 gRPC(或 HTTP、Kafka,或任何服务的输入源)。

然而,有一个问题让我很困扰:日志记录。我认为在 gRPC 处理程序中进行日志记录是合适的,这样我们可以在发生错误时记录错误,或在一切正常时记录信息消息。但是,gRPC 请求通常包含一些额外的数据,如果能将这些数据添加到日志语句中会很好,而这些数据有时需要经过一些处理才能提取出来。一种处理方式是在核心逻辑之前,先在 gRPC 处理程序中部分处理请求,但这会导致消息的部分内容被处理两次,或者需要将部分处理后的消息加上完整消息一起发送给核心逻辑。我认为这两种方式都不理想。

这里有其他人遇到过这个问题吗?有没有好的解决方法?先谢谢了 😊


更多关于Golang Web服务中如何实现带附加字段的日志记录的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

@Gharzol

针对这个问题,一个可能的解决方案是使用一个日志上下文或请求上下文,它可以被传递到核心逻辑操作中。目标是在处理程序中从 gRPC 请求中提取所需数据,将其保存在一个上下文对象中,然后将该上下文对象传递给核心逻辑方法。

以下是一个演示此方法的示例:

  1. 创建一个能够保存相关 gRPC 请求信息的日志上下文对象。这可能是一个基本的数据结构或一个包含必要字段的对象。

  2. 在 gRPC 处理程序中,从请求中提取所需数据并填充到日志上下文对象中。

  3. 在调用核心逻辑函数时,将日志上下文对象传递给它们。

  4. 你可以在核心逻辑函数内部使用这个日志上下文对象,在处理过程中记录重要的信息。

通过使用日志上下文,你可以避免对消息的部分内容进行重复处理,或者将部分处理过的消息与完整消息一起传递给核心逻辑。

希望这能有所帮助!如果你还有任何问题,请随时提出。

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


在Golang Web服务中实现带附加字段的日志记录,可以通过上下文(context.Context)传递日志字段。以下是具体实现方案:

package main

import (
    "context"
    "log/slog"
    "net/http"
)

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

const (
    logFieldsKey contextKey = "log_fields"
)

// LogField 结构体用于存储日志字段
type LogField struct {
    Key   string
    Value any
}

// WithLogFields 向上下文添加日志字段
func WithLogFields(ctx context.Context, fields ...LogField) context.Context {
    existing, ok := ctx.Value(logFieldsKey).([]LogField)
    if !ok {
        existing = []LogField{}
    }
    return context.WithValue(ctx, logFieldsKey, append(existing, fields...))
}

// GetLogFields 从上下文获取日志字段
func GetLogFields(ctx context.Context) []LogField {
    fields, ok := ctx.Value(logFieldsKey).([]LogField)
    if !ok {
        return []LogField{}
    }
    return fields
}

// LoggerWithContext 创建带上下文字段的logger
func LoggerWithContext(ctx context.Context) *slog.Logger {
    fields := GetLogFields(ctx)
    attrs := make([]slog.Attr, len(fields))
    for i, field := range fields {
        attrs[i] = slog.Any(field.Key, field.Value)
    }
    return slog.Default().With(attrs...)
}

// gRPC处理程序示例
func grpcHandler(ctx context.Context, req *YourRequest) (*YourResponse, error) {
    // 从请求中提取需要记录的字段
    userID := extractUserID(req)
    requestID := generateRequestID()
    
    // 将字段添加到上下文
    ctx = WithLogFields(ctx,
        LogField{Key: "user_id", Value: userID},
        LogField{Key: "request_id", Value: requestID},
        LogField{Key: "endpoint", Value: "CreateUser"},
    )
    
    // 调用核心逻辑
    return coreLogic(ctx, req)
}

// 核心逻辑函数
func coreLogic(ctx context.Context, req *YourRequest) (*YourResponse, error) {
    logger := LoggerWithContext(ctx)
    
    // 记录带附加字段的日志
    logger.Info("Processing request")
    
    // 业务逻辑...
    
    if err := someOperation(); err != nil {
        logger.Error("Operation failed", "error", err)
        return nil, err
    }
    
    logger.Info("Request completed successfully")
    return &YourResponse{}, nil
}

// HTTP中间件示例
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        
        // 从HTTP请求中提取字段
        ctx = WithLogFields(ctx,
            LogField{Key: "method", Value: r.Method},
            LogField{Key: "path", Value: r.URL.Path},
            LogField{Key: "ip", Value: r.RemoteAddr},
        )
        
        // 将新上下文传递下去
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

对于结构化日志记录,可以使用slog.Handler实现更精细的控制:

type ContextHandler struct {
    handler slog.Handler
}

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
    // 从上下文获取附加字段
    fields := GetLogFields(ctx)
    for _, field := range fields {
        r.Add(field.Key, field.Value)
    }
    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)}
}

func (h *ContextHandler) Enabled(ctx context.Context, level slog.Level) bool {
    return h.handler.Enabled(ctx, level)
}

// 初始化logger
func initLogger() {
    handler := &ContextHandler{
        handler: slog.NewJSONHandler(os.Stdout, nil),
    }
    slog.SetDefault(slog.New(handler))
}

这种模式允许在gRPC处理程序中提取和附加字段到上下文,然后在核心逻辑中通过上下文获取这些字段并记录到日志中,避免了重复处理请求数据的问题。

回到顶部