Featured image of post Why Can otel.GetTracerProvider() Followed by otel.SetTracerProvider() Still Report Successfully in Go Tracing?

Why Can otel.GetTracerProvider() Followed by otel.SetTracerProvider() Still Report Successfully in Go Tracing?

The process of otel.GetTracerProvider() + otel.SetTracerProvider() and how delegation works in OpenTelemetry for Go

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
}

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

  1. Initial Default: The global tracer provider starts with a default tracerProvider instance (address 0x01)
  2. Early Get: When redisotel/gorm call otel.GetTracerProvider(), they get the initial default provider (0x01)
  3. Late Set: When zrpc calls otel.SetTracerProvider():
    • Creates a delegation chain: initial provider (0x01) → new provider (0x02)
    • Updates globalTracer to the new provider (0x02)
  4. 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.