Recently, when integrating internal tracing in go-zero
using some third-party packages, part of the code is as follows:
package main
import (
"fmt"
"github.com/zeromicro/go-zero/zrpc"
)
func main() {
// 1. Get service configuration
svcCtx := svc.NewServiceContext()
// 2. Initialize server (this actually calls otel.SetTracerProvider())
// server.NewServer() =>
// service.SetUp =>
// trace.StartAgent =>
// trace.startAgent() =>
// otel.SetTracerProvider()
s := zrpc.MustNewServer()
defer s.Stop()
fmt.Printf("Starting rpc server at %s...\n", svcCtx.Config.ListenOn)
s.Start()
}
package svc
import (
"time"
"github.com/redis/go-redis/extra/redisotel/v9"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/plugin/opentelemetry/tracing"
"github.com/zeromicro/go-zero/zrpc"
)
func NewServiceContext() {
conn, err := gorm.Open()
redisClient := redis.NewClient()
// 1.1 Add tracing instrumentation
// Both methods below will call otel.GetTracerProvider()
redisotel.InstrumentTracing(redisClient)
conn.Use(tracing.NewPlugin(tracing.WithoutMetrics()))
// return xxx
}
-
Normally we should
Set
beforeGet
, but OpenTelemetry uses a delegation pattern that allowsGet
beforeSet
.
Code Analysis
- The otel package acts as a wrapper that delegates operations to the global package:
package otel // import "go.opentelemetry.io/otel"
import (
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/trace"
)
func Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
return GetTracerProvider().Tracer(name, opts...)
}
func GetTracerProvider() trace.TracerProvider {
return global.TracerProvider()
}
func SetTracerProvider(tp trace.TracerProvider) {
global.SetTracerProvider(tp)
}
- The global package implements the delegation magic:
package global // import "go.opentelemetry.io/otel/internal/global"
import (
"sync"
"sync/atomic"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
var (
globalTracer = defaultTracerValue()
delegateTraceOnce sync.Once
delegateTextMapPropagatorOnce sync.Once
delegateMeterOnce sync.Once
)
type tracerProviderHolder struct {
tp trace.TracerProvider
}
func TracerProvider() trace.TracerProvider {
return globalTracer.Load().(tracerProviderHolder).tp
}
func SetTracerProvider(tp trace.TracerProvider) {
current := TracerProvider()
if _, cOk := current.(*tracerProvider); cOk {
if _, tpOk := tp.(*tracerProvider); tpOk && current == tp {
// Prevent self-delegation
return
}
}
delegateTraceOnce.Do(func() {
if def, ok := current.(*tracerProvider); ok {
def.setDelegate(tp) // Magic happens here!
}
})
globalTracer.Store(tracerProviderHolder{tp: tp})
}
func defaultTracerValue() *atomic.Value {
v := &atomic.Value{}
v.Store(tracerProviderHolder{tp: &tracerProvider{}})
return v
}
Key Mechanism
- Initial Default: The global tracer provider starts with a default
tracerProvider
instance (address 0x01) - Early Get: When
redisotel
/gorm
callotel.GetTracerProvider()
, they get the initial default provider (0x01) - Late Set: When
zrpc
callsotel.SetTracerProvider()
:- Creates a delegation chain: initial provider (0x01) → new provider (0x02)
- Updates globalTracer to the new provider (0x02)
- Automatic Delegation: The initial default provider forwards all tracing operations to the new provider through
def.setDelegate(tp)
┌───────────────────┐
│ Early Get │
│ (gorm/redis) │
│ GetTracerProvider │─────┐
└───────────────────┘ │
│
▼
┌─────────────────────┐
│ Default TracerProvider (0x01)
│ (delegate: nil)
└─────────────────────┘
▲
│
┌───────────────────┐ │
│ Late Set │ │
│ (zrpc) │ │
│ SetTracerProvider│─────┘
└───────────────────┘
│
▼
┌─────────────────────┐
│ New TracerProvider (0x02)
└─────────────────────┘
▲
│
┌─────┴─────┐
│ Delegation│
│ Established
└───────────┘
Why It Works?
- Atomic Operations: The
globalTracer
uses atomic value for thread-safe access - Delegation Pattern: The initial default provider acts as a proxy that forwards operations to the latest provider
- Sync.Once: Ensures delegation setup only happens once
This elegant design allows instrumentation libraries to safely initialize tracing components before the actual tracer provider is configured.