tgf v2 可观测性接入指南

June 11, 2026 · View on GitHub

最后更新:2026-04-11

tgf v2 在 B4 档给框架补齐了三个可观测性抽象:metrics / trace / log。本 文档说明如何给业务项目接入实际的监控基建(Prometheus / Grafana / OpenTelemetry / Jaeger / Loki)。

核心原则:tgf 框架本身零监控依赖——默认全部是 NoOp 实现。业务方按 自己已有的基建选择 adapter,接入点都是 Server.WithMetrics(p) / Server.WithTracer(t) 这类 builder 方法。

三个维度快览

维度默认实现推荐接入
指标(metrics)tgf/metricsNoopProviderPrometheus / StatsD
追踪(trace)tgf/traceNoopTracerOpenTelemetry / Jaeger
日志(log)tgf/logzap + lumberjack stdout + fileLoki / ELK

metrics 接入

1. 理解接口

package metrics

type Counter interface {
    Inc()
    Add(delta float64)
}
type Gauge interface {
    Inc()
    Dec()
    Add(delta float64)
    Set(value float64)
}
type Histogram interface {
    Observe(value float64)
}
type Provider interface {
    Counter(name, help string, labelNames ...string) Counter
    Gauge(name, help string, labelNames ...string) Gauge
    Histogram(name, help string, buckets []float64, labelNames ...string) Histogram
    Name() string
}

2. 框架内部已有埋点

这些在 v2 自动触发,用户只要接了 Provider 就能看到:

指标类型含义
tgf_gate_connectionsGauge网关当前活跃连接数
tgf_rpc_latency_msHistogramRPC 端到端延迟(ms)
tgf_rpc_calls_totalCounterRPC 累计调用次数
tgf_rpc_calls_fail_totalCounterRPC 累计失败次数
tgf_rpc_policy_reject_circuit_totalCounter断路器拒绝次数
tgf_rpc_policy_reject_ratelimit_totalCounter限流拒绝次数
tgf_rpc_policy_reject_concurrency_totalCounter超并发拒绝次数

3. 接入 Prometheus(推荐)

业务方写一个独立 subpackage(tgf 不提供,避免拖依赖):

// your-repo/tgf-prom/provider.go
package tgfprom

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/thkhxm/tgf/v2/metrics"
)

type Provider struct {
    namespace string
    reg       prometheus.Registerer
}

func NewProvider(namespace string) *Provider {
    return &Provider{
        namespace: namespace,
        reg:       prometheus.DefaultRegisterer,
    }
}

func (p *Provider) Counter(name, help string, _ ...string) metrics.Counter {
    c := prometheus.NewCounter(prometheus.CounterOpts{
        Namespace: p.namespace,
        Name:      name,
        Help:      help,
    })
    p.reg.MustRegister(c)
    return &promCounter{c}
}

func (p *Provider) Gauge(name, help string, _ ...string) metrics.Gauge {
    g := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: p.namespace,
        Name:      name,
        Help:      help,
    })
    p.reg.MustRegister(g)
    return &promGauge{g}
}

func (p *Provider) Histogram(name, help string, buckets []float64, _ ...string) metrics.Histogram {
    h := prometheus.NewHistogram(prometheus.HistogramOpts{
        Namespace: p.namespace,
        Name:      name,
        Help:      help,
        Buckets:   buckets,
    })
    p.reg.MustRegister(h)
    return &promHistogram{h}
}

func (p *Provider) Name() string { return "prometheus" }

// --- Counter wrapper ---
type promCounter struct{ c prometheus.Counter }
func (p *promCounter) Inc()              { p.c.Inc() }
func (p *promCounter) Add(v float64)     { if v > 0 { p.c.Add(v) } }

// --- Gauge wrapper ---
type promGauge struct{ g prometheus.Gauge }
func (p *promGauge) Inc()          { p.g.Inc() }
func (p *promGauge) Dec()          { p.g.Dec() }
func (p *promGauge) Add(v float64) { p.g.Add(v) }
func (p *promGauge) Set(v float64) { p.g.Set(v) }

// --- Histogram wrapper ---
type promHistogram struct{ h prometheus.Histogram }
func (p *promHistogram) Observe(v float64) { p.h.Observe(v) }

然后在业务 main.go 里:

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/thkhxm/tgf/v2/rpc"
    tgfprom "your-repo/tgf-prom"
)

func main() {
    // 启动 /metrics endpoint
    go http.ListenAndServe(":9100", promhttp.Handler())

    rpc.NewRPCServer().
        WithMetrics(tgfprom.NewProvider("myapp")).
        WithService(myService).
        Run()
}

4. 接入 StatsD / DataDog / 自定义存储

同样的模式——实现 metrics.Provider 接口即可。几十行代码。

5. 自定义埋点

业务代码可以直接用 metrics 包的便捷函数:

import "github.com/thkhxm/tgf/v2/metrics"

var onlineUsers = metrics.NewGauge("online_users", "当前在线用户数")

func OnUserLogin(uid string) {
    onlineUsers.Inc()
}
func OnUserLogout(uid string) {
    onlineUsers.Dec()
}

这些 gauge/counter 会自动走当前注入的 Provider。


trace 接入

1. 接口

package trace

type Span interface {
    SetAttribute(key string, value any)
    SetError(err error)
    End()
    TraceID() string
}
type Tracer interface {
    StartSpan(ctx context.Context, name string) (context.Context, Span)
    Name() string
}

2. 默认行为

默认 NoopTracer 仍然生成 trace id 并放到 context 里,业务日志可以用 trace.TraceIDFromContext(ctx) 读出来做日志串联——即使不接任何追踪基建, trace id 也是可用的。

3. 接入 OpenTelemetry(推荐)

写一个 adapter 把 go.opentelemetry.io/otel 的 Tracer 包装成 tgf/trace.Tracer

// your-repo/tgf-otel/tracer.go
package tgfotel

import (
    "context"
    otelsdk "go.opentelemetry.io/otel"
    oteltrace "go.opentelemetry.io/otel/trace"
    "github.com/thkhxm/tgf/v2/trace"
)

type Tracer struct {
    inner oteltrace.Tracer
}

func NewTracer(serviceName string) *Tracer {
    return &Tracer{inner: otelsdk.Tracer(serviceName)}
}

func (t *Tracer) StartSpan(ctx context.Context, name string) (context.Context, trace.Span) {
    newCtx, span := t.inner.Start(ctx, name)
    return newCtx, &spanAdapter{span: span}
}

func (t *Tracer) Name() string { return "otel" }

type spanAdapter struct {
    span oteltrace.Span
}
func (s *spanAdapter) SetAttribute(k string, v any) {
    // 按类型分发到 otel attribute helpers
}
func (s *spanAdapter) SetError(err error) { s.span.RecordError(err) }
func (s *spanAdapter) End()                { s.span.End() }
func (s *spanAdapter) TraceID() string     {
    return s.span.SpanContext().TraceID().String()
}

4. trace id 透传

tgf 已经把 trace id 塞进 rpcx 的 share.ContextContextKeyTRACEID 字段, 下游服务可以通过 rpc.GetUserId(ctx) 类似的 helper 读出来。配合 OpenTelemetry 的 W3C TraceContext 传播器,可以实现完整的分布式链路。


日志接入(B5)

1. 热路径优化

v2 的 log.*Tag 系列现在会先做 level + tag 过滤,再决定要不要 fmt.Sprintf。 日志级别设置为 WARN 时所有 Debug* / Info* 调用零分配开销。

2. zap.Field 风格 API(推荐新代码)

log.DebugTagW("gate", "连接接入",
    zap.String("addr", addr),
    zap.Int("fd", fd),
    zap.Duration("latency", elapsed),
)

相比老 log.DebugTag("gate", "连接接入 addr=%s fd=%d latency=%v", ...)

  • fmt.Sprintf 分配
  • 无 interface{} 装箱
  • 日志收集器(Loki / ELK)能按字段结构化索引

3. 接入 Loki / ELK

tgf 的 zap core 用 JSON encoder 输出文件日志(logs/*.log),可以直接接 Filebeat / Promtail。每条日志包含:

{
  "time": "2026-04-11 20:30:00.000",
  "level": "INFO",
  "caller": "[rpc/tcp.go:451]",
  "message": "接收到一条新的连接",
  "tag": "tcp",
  "addr": "10.0.0.1:52134",
  "fd": 27
}

业务只需要在 Loki / ELK 侧配置按 tag / level / trace_id 做 label 索引即可。


完整接入示例

package main

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/thkhxm/tgf/v2/config"
    "github.com/thkhxm/tgf/v2/log"
    "github.com/thkhxm/tgf/v2/rpc"
    "github.com/thkhxm/tgf/v2/trace"
    tgfotel "your-repo/tgf-otel"
    tgfprom "your-repo/tgf-prom"
)

func main() {
    // 1. 加载配置
    cfg, err := config.Load()
    if err != nil {
        log.ErrorW("配置加载失败", zap.Error(err))
        return
    }

    // 2. metrics server
    go http.ListenAndServe(":9100", promhttp.Handler())

    // 3. 监听配置热更
    config.OnReload(func(c *config.Config) {
        log.InfoW("配置热更触发", zap.String("level", c.Logger.Level))
    })

    // 4. 启动 tgf server
    rpc.NewRPCServer().
        WithMetrics(tgfprom.NewProvider("myapp")).
        WithTracer(tgfotel.NewTracer("myapp")).
        WithMethodPolicy("gate.Login", rpc.MethodPolicy{
            Timeout:   3 * time.Second,
            RateLimit: 500,
            CircuitBreaker: rpc.CircuitBreakerConfig{
                FailureThreshold: 5,
                RecoveryInterval: 30 * time.Second,
            },
        }).
        WithHealthCheck(10 * time.Second).
        WithGatewayOptions(rpc.GatewayOptions{
            TCPPort: cfg.Service.Port,
            WSPath:  "/ws",
        }).
        WithService(myService).
        Run()
}

参考