example_service/

directory
v0.3.15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0

README

示例概览:example_service 多服务链路追踪演示

example_service 目录下包含一组互相调用的示例服务,用于演示:

  • 多个 HTTP 服务之间的调用链路(网关 -> 订单 -> 支付 -> 用户 等)
  • 如何在每个服务里初始化 Jaeger 链路追踪
  • 如何在服务之间通过 rest.Client 进行调用,并接入自定义的 OpenTelemetry Hook(NewReqxOtelHook
  • 同时演示 gin 和 go-restful 两种 HTTP 框架的使用方式

服务列表(端口均不冲突):

  • gateway:网关服务,对外统一入口,端口 18080
  • user:用户服务,提供用户基础信息查询,端口 18081
  • order:订单服务,提供订单列表查询,端口 18082
  • payment:支付服务,提供订单支付状态查询,端口 18083

调用关系与链路结构

整体调用链大致如下(从外部调用网关开始):

sequenceDiagram
    participant Client as 外部调用方
    participant Gateway as gateway-service (gin)
    participant User as user-service (gin)
    participant Order as order-service (go-restful)
    participant Payment as payment-service (gin)

    Client->>Gateway: HTTP /api/checkout/:userID
    Gateway->>User: HTTP GET /api/v1/user/:id
    Gateway->>Order: HTTP GET /api/v1/orders?userID=
    Order->>Payment: HTTP GET /api/v1/payment/:orderID?userID=
    Payment->>User: HTTP GET /api/v1/user/:id

在 Jaeger UI 中,你应该能够看到一条完整的 Trace,从外部请求网关开始,一路串到多个下游服务,中间的所有出站 HTTP 调用都会作为子 Span 展示出来。


各服务职责与实现要点

1. 网关服务:gateway

  • 文件位置:[main.go](/rest/example/example_service/gateway/main.go)
  • 监听地址:":18080"
  • 主要职责:
    • 对外暴露 GET /api/checkout/:userID 接口
    • 聚合 user-serviceorder-service 的数据,返回一个包含用户信息和订单列表的响应
  • 链路追踪初始化:
    • 使用 jaeger.InitTracer(ctx, "gateway-service") 初始化当前进程的 TracerProvider
    • 使用 otelgin.Middleware("gateway-service-gin") 为 gin 路由接入 Server 端链路追踪中间件
  • rest.Client 的使用:
    • 通过 otel.Tracer("gateway-service-goto-next") 获取一个 trace.Tracer
    • 创建基础客户端:
      tracer := otel.Tracer("gateway-service-goto-next")
      baseClient := rest.NewDefaultClient().
          SetRequestHook(trace.NewReqxOtelHook(tracer))
      
    • 每次处理请求时通过 baseClient.Clone() 克隆一份客户端,避免并发场景下修改配置互相影响。
    • 调用下游服务示例:
      resp := client.SetBaseURL(userServiceBase).
          Get("/api/v1/user/" + userID).
          Do(ctx)
      
    • NewReqxOtelHook 会在每一个出站 HTTP 请求前后:
      • 自动创建 Client Span
      • 注入 Trace 上下文到请求头
      • 记录 HTTP 方法、URL 等语义属性
      • 从 Span 中提取 TraceID 打日志,可选地写入自定义请求头(供老系统使用)

2. 用户服务:user

  • 文件位置:[main.go](/rest/example/example_service/user/main.go)
  • 监听地址:":18081"
  • 主要职责:
    • 对外暴露 GET /api/v1/user/:id 接口
    • 返回一个简单的用户信息结构体(示例中使用静态拼接)
  • 链路追踪使用:
    • 同样使用 jaeger.InitTracer(ctx, "user-service") 初始化当前服务的 Tracer
    • 使用 otelgin.Middleware("user-service-gin") 对 gin 的入口进行自动埋点
  • rest.Client 的使用:
    • 用户服务本身 不再向下游发 HTTP 请求,因此这里没有用到 rest.Client
    • 它主要作为 “被调用方” 出现在调用链中。

3. 订单服务:order

  • 文件位置:[main.go](/rest/example/example_service/order/main.go)
  • 监听地址:":18082"
  • 使用框架:go-restful
  • 主要职责:
    • 对外暴露 GET /api/v1/orders?userID=xxx 接口
    • 为指定用户生成一组模拟订单,并在内部继续调用支付服务与用户服务丰富信息
  • 链路追踪初始化:
    • 使用 jaeger.InitTracer(ctx, "order-service") 初始化当前服务的 Tracer
    • 使用 otel.Tracer("order-service-go-restful")getOrders 处理函数内手动创建 Server Span:
      ctx := req.Request.Context()
      propagator := otel.GetTextMapPropagator()
      ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Request.Header))
      tracer := otel.Tracer("order-service-go-restful")
      ctx, span := tracer.Start(ctx, "GET /api/v1/orders")
      
  • rest.Client 的使用:
    • main 中创建带 Hook 的全局客户端:
      tracer := otel.Tracer("order-service-goto-next")
      httpClient = rest.NewDefaultClient().
          SetRequestHook(trace.NewReqxOtelHook(tracer))
      
    • getOrders 中,为每个请求克隆一份客户端:
      client := httpClient.Clone()
      
    • 调用 payment-serviceuser-service
      // 调支付服务
      resp1 := client.SetBaseURL(paymentServiceBase).
          Get("/api/v1/payment/"+orders[i].ID).
          Param("userID", userID).
          Do(ctx)
      
      // 调用户服务
      resp2 := client.SetBaseURL(userServiceBase).
          Get("/api/v1/user/" + userID).
          Do(ctx)
      
    • 通过 NewReqxOtelHook,这些出站 HTTP 调用会自动出现在 Trace 中,形成: gateway -> order-service -> payment-service -> user-service 的完整链路。

4. 支付服务:payment

  • 文件位置:[main.go](/rest/example/example_service/payment/main.go)
  • 监听地址:":18083"
  • 使用框架:gin
  • 主要职责:
    • 对外暴露 GET /api/v1/payment/:orderID?userID=xxx 接口
    • 模拟判断某订单的支付状态
    • 内部再调用一次 user-service,丰富返回结果中的用户信息
  • 链路追踪初始化:
    • 使用 jaeger.InitTracer(ctx, "payment-service")
    • 使用 otelgin.Middleware("payment-service-gin") 接入 gin 中间件
  • rest.Client 的使用:
    • main 中初始化全局 httpClient
      tracer := otel.Tracer("payment-service-goto-next")
      httpClient = rest.NewDefaultClient().
          SetRequestHook(trace.NewReqxOtelHook(tracer))
      
    • 在处理函数中 Clone() 出一份请求级客户端,然后调用 user-service
      client := httpClient.Clone()
      resp := client.SetBaseURL(userServiceBase).
          Get("/api/v1/user/" + userID).
          Do(ctx)
      

关于 rest.Client 的使用要点

  • 初始化

    • 推荐在服务启动时创建一个“基础客户端”,配置好超时、重试策略、链路追踪 Hook 等。
    • 示例中使用:
      baseClient := rest.NewDefaultClient().
          SetRequestHook(trace.NewReqxOtelHook(tracer))
      
  • 每请求克隆

    • 由于一个 rest.Client 在使用时,会不断通过链式调用修改内部状态(SetBaseURLHeaderParam 等),所以 不要在多个请求之间直接复用同一个实例
    • 正确方式是:
      client := baseClient.Clone()
      
    • 这样可以在并发场景下保证不同请求之间互不干扰。
  • 链路追踪 Hook

    • trace.NewReqxOtelHook(tracer) 会在每个请求时:
      • 创建 SpanKindClient 类型的 Span
      • 打上标准 HTTP 语义属性(如方法、URL、状态码等)
      • 将 Trace 上下文注入 HTTP 头部(W3C Trace Context 格式)
      • 从 Span 提取 TraceID 打日志,并可写入自定义请求头(如 X-Trace-ID)供老系统使用
  • 与 gin / go-restful 中间件配合

    • gin 侧用 otelgin.Middleware(...) 自动为入口请求创建 Server Span
    • go-restful 侧示例中展示了如何手动从请求头中 Extract 出上下文,并创建 Span
    • rest.Client 会基于这些上下文继续往下传播 Trace,实现“端到端”的链路串联。

注意事项与最佳实践

  • 1. 每个服务只初始化一次 TracerProvider

    • 每个示例服务的 main 函数里,都调用了一次 jaeger.InitTracer(ctx, "xxx-service"),并在退出时调用 shutdown
    • 真实生产环境下也应遵循“每进程一次初始化”的原则,不要在每个请求中重复创建。
  • 2. 不要双重埋点同一条出站请求

    • 你可以选择:
      • 使用 NewReqxOtelHook(示例代码当前使用)
      • 或者使用 otelhttp.NewTransport 来包装底层 http.RoundTripper
    • 不要同时使用两种方式,否则可能在同一次 HTTP 调用上生成两个 Client Span。
  • 3. 始终使用请求上下文 ctx

    • 示例中所有 .Do(ctx) 调用都传入了从 gin / go-restful handler 中获取的 c.Request.Context()req.Request.Context()
    • 这是保证 Trace 能够串起来的关键:Span 和日志都会挂载在正确的调用链上。
  • 4. TraceID 的获取与利用

    • NewReqxOtelHook 中,你可以通过 span.SpanContext().TraceID() 拿到当前 Trace 的 ID,进而:
      • 记录到结构化日志中,方便排查问题时在日志与 Jaeger 之间对照
      • 写入 X-Trace-ID 等自定义 HTTP 头,给不支持 OpenTelemetry 的下游系统使用
  • 5. 端口与路径规划

    • 所有服务的端口在示例中已经错开,方便在本机直接运行多进程:
      • gateway: 18080
      • user: 18081
      • order: 18082
      • payment: 18083
    • 路径命名尽量贴近真实业务接口风格,便于你在自己的项目中直接参考迁移。

如何运行与观察效果(简要)

  1. 启动本地 Jaeger(例如使用 Docker 或已有环境)。
  2. 依次在不同终端启动:userpaymentordergateway 四个服务。
  3. 通过浏览器或 curl 调用网关接口,例如:
    curl "http://localhost:18080/api/checkout/123"
    
  4. 打开 Jaeger UI,按 service=gateway-service 或具体 TraceID 查询,就可以看到完整的多服务调用链。

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL