每日一Go-54、Go微服务--日志与链路追踪初探

0 阅读5分钟

Go微服务的日志比较经典的一句话:你不是在写日志,你是在给未来的自己留活路。在微服务里,日志等于事后复盘,链路追踪等于实时破案。

一、微服务里,日志到底要解决什么?

1. 微服务日志的3个核心问题

问题本质
日志太多没结构
日志没用没上下文
日志对不上没TraceID    

结论是:没有TraceID的日志,等于没写。

二、统一日志规范

必须满足5点:

结构化日志(JSON)

自动携带TraceID

支持分级(Debug/Info/Error)

全链路统一格式

方便ELK/Loki收集

推荐技术栈

角色选择
日志库zap
Web框架gin
TraceIDOpenTelemetry
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”获取源码

2、pan.baidu.com/s/1B6pgLWfS…


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!