在微服务架构下,每个服务独立部署运行,请求会在多个服务之间流转,分散的日志会给问题排查带来很大困难。统一记录请求信息,把请求路径、来源IP、响应耗时、状态码等关键数据整合到日志中,能让日志的价值最大化。Golang生态中有很多成熟的日志库,结合中间件机制可以很方便地实现这个需求。

核心实现思路
实现统一请求日志收集的核心逻辑是借助HTTP中间件,在请求进入业务逻辑前和返回响应后分别做处理,提取请求和响应的相关信息,再调用日志库输出。整体流程分为三步:
- 编写中间件函数,拦截所有HTTP请求
- 在中间件中记录请求开始时间,提取请求参数、Header等信息
- 业务逻辑处理完成后,计算请求耗时,整合响应状态码等信息,统一输出日志
基础实现示例
下面使用Golang标准库的net/http和常用的日志库logrus来实现基础的请求日志收集功能。
中间件实现
首先定义日志中间件,负责拦截请求并收集信息:
package main
import (
"log"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
// 日志中间件结构体,可注入自定义日志实例
type LogMiddleware struct {
logger *logrus.Logger
}
// 中间件处理函数
func (m *LogMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录请求开始时间
startTime := time.Now()
// 包装ResponseWriter,用于获取响应状态码
wrapWriter := &responseWrapper{ResponseWriter: w, statusCode: http.StatusOK}
// 执行后续业务逻辑
next.ServeHTTP(wrapWriter, r)
// 计算请求耗时
duration := time.Since(startTime)
// 提取请求信息
requestInfo := map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"client_ip": r.RemoteAddr,
"status": wrapWriter.statusCode,
"duration": duration.Milliseconds(),
"user_agent": r.UserAgent(),
}
// 根据状态码分级输出日志
if wrapWriter.statusCode >= 500 {
m.logger.WithFields(logrus.Fields(requestInfo)).Error("请求处理异常")
} else if wrapWriter.statusCode >= 400 {
m.logger.WithFields(logrus.Fields(requestInfo)).Warn("请求参数错误")
} else {
m.logger.WithFields(logrus.Fields(requestInfo)).Info("请求处理成功")
}
})
}
// 包装ResponseWriter,用于获取响应状态码
type responseWrapper struct {
http.ResponseWriter
statusCode int
}
// 重写WriteHeader方法,记录状态码
func (w *responseWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
业务服务集成
将中间件注册到HTTP服务中,即可对所有请求生效:
func main() {
// 初始化日志实例
logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 创建中间件实例
middleware := &LogMiddleware{logger: logger}
// 注册业务路由
mux := http.NewServeMux()
mux.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
// 模拟业务逻辑处理
time.Sleep(100 * time.Millisecond)
w.Write([]byte("获取用户信息成功"))
})
mux.HandleFunc("/api/order", func(w http.ResponseWriter, r *http.Request) {
// 模拟返回错误
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("参数错误"))
})
// 使用中间件包裹路由处理器
handler := middleware.Middleware(mux)
// 启动服务
log.Println("服务启动在 :8080 端口")
if err := http.ListenAndServe(":8080", handler); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
日志输出扩展
上面的示例只是把日志输出到控制台,实际生产中通常需要把日志输出到文件,或者发送到远程日志系统比如ELK、Loki等。以logrus为例,添加文件输出非常简单:
import (
"os"
"github.com/sirupsen/logrus"
)
func initLogger() *logrus.Logger {
logger := logrus.New()
// 设置日志格式为JSON
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 同时输出到文件和标准输出
file, err := os.OpenFile("service.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
logger.SetOutput(file)
} else {
logrus.Warn("打开日志文件失败,使用默认输出")
}
return logger
}
注意事项
- 如果请求体较大,不要直接读取全部请求体到日志中,避免日志体积过大,只记录必要的内容即可
- 敏感信息比如用户密码、token等不要记录到日志中,防止信息泄露
- 高并发场景下,可以选择异步输出日志,避免日志写入阻塞业务逻辑执行
- 不同微服务的日志最好添加统一的trace_id,方便追踪跨服务的请求链路
统一记录请求信息的日志收集方案,能让微服务架构下的日志管理更规范,后续结合日志查询系统可以快速定位问题,提升系统可维护性。