Go微服务的日志比较经典的一句话:你不是在写日志,你是在给未来的自己留活路。在微服务里,日志等于事后复盘,链路追踪等于实时破案。
一、微服务里,日志到底要解决什么?
1. 微服务日志的3个核心问题
| 问题 | 本质 |
| 日志太多 | 没结构 |
| 日志没用 | 没上下文 |
| 日志对不上 | 没TraceID |
结论是:没有TraceID的日志,等于没写。
二、统一日志规范
必须满足5点:
结构化日志(JSON)
自动携带TraceID
支持分级(Debug/Info/Error)
全链路统一格式
方便ELK/Loki收集
推荐技术栈
| 角色 | 选择 |
| 日志库 | zap |
| Web框架 | gin |
| TraceID | OpenTelemetry |
| Trace后端 | Jaeger/Tempo |
三、Gin+Zap:企业级日志初始化
1. 日志初始化
package logger
import "go.uber.org/zap"
// Log 是全局的zap日志实例
var Log *zap.Logger
// Init 初始化全局日志实例
// 配置使用生产级别日志格式,并将输出重定向到标准输出
func Init() {
// 创建生产级别日志配置
cfg := zap.NewProductionConfig()
// 设置日志输出路径为标准输出
cfg.OutputPaths = []string{"stdout"}
// 构建日志实例
var err error
Log, err = cfg.Build()
if err != nil {
// 如果初始化失败,直接panic
panic(err)
}
}
四、TraceID是怎么贯穿整个系统的?
一次请求=一根“钢丝”
Client
↓
API Gateway
↓
User Service
↓
Order Service
↓
DB / Redis
TraceID就是这根钢丝。
五、OpenTelemetry:自动生成TraceID
1. 初始化Tracer
package tracing
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// Init 初始化 OpenTelemetry 跟踪系统
// serviceName: 服务名称,用于在分布式追踪系统中标识当前服务
func Init(serviceName string) {
// 创建带超时的上下文,防止初始化过程阻塞过久
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// 创建 OTLP HTTP 导出器,将跟踪数据发送到 Jaeger 服务器
exp, err := otlptracehttp.New(
ctx,
// 设置 Jaeger 服务器端点
otlptracehttp.WithEndpoint("jaeger:4318"),
// 允许使用非安全连接(生产环境建议使用安全连接)
otlptracehttp.WithInsecure(),
)
if err != nil {
// 如果导出器创建失败,直接panic
panic(err)
}
// 创建跟踪提供器(TracerProvider)
tp := sdktrace.NewTracerProvider(
// 使用批处理器发送跟踪数据,提高性能
sdktrace.WithBatcher(exp),
// 设置资源属性,包括服务名称
sdktrace.WithResource(
resource.NewWithAttributes(
"",
attribute.String("service.name", serviceName),
),
),
)
// 设置全局跟踪提供器
otel.SetTracerProvider(tp)
}
六、Gin中间件:日志+TraceID注入
1. Trace+Log中间件
package middleware
import (
"day54/logger"
"time"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.uber.org/zap"
)
// TraceLog 返回一个 Gin 中间件,用于为 HTTP 请求添加跟踪日志
// 该中间件会创建 OpenTelemetry 跟踪 span,并记录请求的关键信息
func TraceLog() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取 HTTP 跟踪器
tracer := otel.Tracer("http")
// 为当前请求创建一个 span,使用请求的完整路径作为 span 名称
ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
// 确保请求结束后关闭 span
defer span.End()
// 获取跟踪 ID
traceID := span.SpanContext().TraceID().String()
// 将跟踪 ID 存储到 Gin 上下文中,以便后续处理函数使用
c.Set("trace_id", traceID)
// 更新请求的上下文,包含跟踪信息
c.Request = c.Request.WithContext(ctx)
// 记录请求开始时间
start := time.Now()
// 调用后续中间件和处理函数
c.Next()
// 记录请求日志,包含跟踪 ID、路径、状态码和处理时间
logger.Log.Info("http request",
zap.String("trace_id", traceID),
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.Duration("cost", time.Since(start)),
)
}
}
七、业务日志应该长什么样?
1. 错误示例
log.Println("create order failed")
2. 正确示例
traceID, _ := c.Get("trace_id")
logger.Log.Error("create order failed",
zap.String("trace_id", traceID.(string)),
zap.Int64("user_id", userID),
zap.Error(err),
)
任何Error日志,必须回答以下问题:
-
谁?
-
什么时候?
-
干了什么?
-
为什么失败?
-
属于哪次请求?
八、main.go
package main
import (
"day54/logger"
"day54/middleware"
"day54/tracing"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// main 函数是应用程序的入口点
func main() {
// 初始化日志系统
logger.Init()
// 初始化跟踪系统,服务名称为 "log-tracer"
tracing.Init("log-tracer")
// 创建 Gin 引擎实例(不使用默认中间件)
r := gin.New()
// 添加恢复中间件,防止 panic 导致服务崩溃
r.Use(gin.Recovery())
// 添加跟踪日志中间件,为每个请求添加跟踪信息
r.Use(middleware.TraceLog())
// 定义 /ping 路由的 GET 请求处理函数
r.GET("/ping", func(c *gin.Context) {
// 从 Gin 上下文中获取跟踪 ID
traceID, _ := c.Get("trace_id")
// 记录 ping 请求日志
logger.Log.Info("ping called",
zap.String("trace_id", traceID.(string)),
)
// 返回 JSON 响应,包含 "pong" 消息和跟踪 ID
c.JSON(http.StatusOK, gin.H{
"msg": "pong",
"trace_id": traceID,
})
})
// 启动 HTTP 服务器,监听 8080 端口
r.Run(":8080")
}
九、启动&验证
1. docker-compose.yml
# 定义 Docker 服务
version: '3.8'
services:
# Jaeger 分布式追踪服务
jaeger:
# 使用 Jaeger all-in-one 镜像,包含完整的追踪功能
image: jaegertracing/all-in-one:1.52
# 容器名称
container_name: jaeger
# 端口映射
ports:
- "16686:16686" # Jaeger UI 端口
- "4318:4318" # OTLP HTTP 协议端口
- "4317:4317" # OTLP gRPC 协议端口
# 环境变量配置
environment:
- COLLECTOR_OTLP_ENABLED=true # 启用 OTLP 收集器
# Go 应用程序服务
app:
build: ./
# 容器名称
container_name: gin-app
# 端口映射,将容器的 8080 端口映射到主机的 8080 端口
ports:
- "8080:8080"
# 依赖关系,确保 jaeger 服务先启动
depends_on:
- jaeger
2. Dockerfile
FROM golang:1.25.5-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
COPY . .
RUN go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
3. 启动服务
docker-compose up -d
4. 启动后的访问地址:
Jaeger UI: http://localhost:16686
Gin API: http://localhost:8080
5. 请求接口
$ curl http://localhost:8080/ping
{"msg":"pong","trace_id":"a91d2be3973d72316178a506ce171217"}
6. 打开JaegerUI http://localhost:16686 查看
十、人生比喻
日志,是你出问题之后的日记;链路追踪,是你做事时的录像。
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!