go-redis/redis · error

redisotel: already initialized, call Shutdown() before reini

Error message

redisotel: already initialized, call Shutdown() before reinitializing

What it means

Returned by ObservabilityInstance.Init when the global OpenTelemetry observability singleton has already been initialized and Init is called again without an intervening Shutdown(). The singleton (guarded by observabilityInstanceOnce) attaches a metrics recorder to all Redis clients via redis.SetOTelRecorder, so a second Init would race with in-flight clients.

Source

Thrown at extra/redisotel-native/redisotel.go:77

// GetObservabilityInstance returns the global observability singleton.
func GetObservabilityInstance() *ObservabilityInstance {
	observabilityInstanceOnce.Do(func() {
		observabilityInstance = &ObservabilityInstance{}
	})
	return observabilityInstance
}

// Init initializes OpenTelemetry observability globally for all Redis clients.
// This should be called once at application startup, BEFORE creating any Redis clients.
// After initialization, all Redis clients will automatically collect and export
// metrics without needing any additional configuration.
func (o *ObservabilityInstance) Init(cfg *Config) error {
	o.mu.Lock()
	defer o.mu.Unlock()

	// If already initialized, return error
	if o.initialized {
		return errors.New("redisotel: already initialized, call Shutdown() before reinitializing")
	}

	o.config = cfg

	if !cfg.Enabled {
		return nil
	}

	// Get meter provider (use global if not provided)
	meterProvider := cfg.MeterProvider
	if meterProvider == nil {
		meterProvider = otel.GetMeterProvider()
	}

	meter := meterProvider.Meter(
		"github.com/redis/go-redis",
		metric.WithInstrumentationVersion(redis.Version()),
	)

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Call Init exactly once at process startup; guard with IsEnabled() before calling.
  2. In tests or reload paths, call GetObservabilityInstance().Shutdown() before re-Init.
  3. If you need isolated instances, refactor to avoid the global singleton for tests.

Example fix

// before
redisotel.GetObservabilityInstance().Init(cfg1)
redisotel.GetObservabilityInstance().Init(cfg2) // errors
// after
inst := redisotel.GetObservabilityInstance()
if !inst.IsEnabled() {
    inst.Init(cfg)
}
Defensive patterns

Strategy: validation

Validate before calling

inst := redisotel.GetObservabilityInstance()
if !inst.IsEnabled() {
    if err := inst.Init(cfg); err != nil {
        return err
    }
}

Type guard

func mustInitObservability(cfg *redisotel.Config) error {
    inst := redisotel.GetObservabilityInstance()
    if inst.IsEnabled() {
        return nil
    }
    return inst.Init(cfg)
}

Try / catch

if err := inst.Init(cfg); err != nil {
    if strings.Contains(err.Error(), "already initialized") {
        return nil // acceptable: already set up
    }
    return err
}

Prevention

When it happens

Trigger: Calling redisotel.GetObservabilityInstance().Init(cfg) twice in the same process — common in hot-reload (e.g. Air/plug, Neko dev-mode), tests that re-init per case without Shutdown, or shared-library code that initializes on import and again in main().

Common situations: Test suites reusing the global instance, serverless warm-starts that re-run setup, or multiple modules each calling Init defensively.

Related errors


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/eb7272f4c2d65877.json. Report an issue: GitHub.