Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
autoexport: Change WithFallback options signatures
### Problem Statement

The current definition of fallback options is as following:

```go
func WithFallbackMetricReader(exporter metric.Reader) MetricOption
```

The problem is that we need to create an instance of a metric reader or span exporter and it may not even be needed/used (it may even leak resources).

### Proposed Solution

Change the signature to accept a factory function e.g.

```go
func WithFallbackMetricReader(factory func(context.Context) (metric.Reader, error)) MetricOption
```


Pull request:
feat(autoexport): change WithFallback options signatures
Fixes #4877.

Changes the method signatures as requested in the issue.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f869f53c..c416a72ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Add client metric support to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#4707)
 - Add peer attributes to spans recorded by `NewClientHandler`, `NewServerHandler` in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`. (#4873)
 
+### Changed
+
+- The fallback options in  `go.opentelemetry.io/contrib/exporters/autoexport` now accept factory functions. (#4891)
+  - `WithFallbackMetricReader(metric.Reader) MetricOption` is replaced with `func WithFallbackMetricReader(func(ctx context.Context) (metric.Reader, error)) MetricOption`.
+  - `WithFallbackSpanExporter(trace.SpanExporter) SpanOption` is replaced with `WithFallbackSpanExporter(func(ctx context.Context) (trace.SpanExporter, error)) SpanOption`.
+
 ### Deprecated
 
 - The `RequestCount`, `RequestContentLength`, `ResponseContentLength`, `ServerLatency` constants in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` are deprecated. (#4707)
diff --git a/exporters/autoexport/metrics.go b/exporters/autoexport/metrics.go
index 1cfbbbc78..217f75f34 100644
--- a/exporters/autoexport/metrics.go
+++ b/exporters/autoexport/metrics.go
@@ -39,8 +39,8 @@ type MetricOption = option[metric.Reader]
 
 // WithFallbackMetricReader sets the fallback exporter to use when no exporter
 // is configured through the OTEL_METRICS_EXPORTER environment variable.
-func WithFallbackMetricReader(exporter metric.Reader) MetricOption {
-	return withFallback[metric.Reader](exporter)
+func WithFallbackMetricReader(metricReaderFactory func(ctx context.Context) (metric.Reader, error)) MetricOption {
+	return withFallbackFactory[metric.Reader](metricReaderFactory)
 }
 
 // NewMetricReader returns a configured [go.opentelemetry.io/otel/sdk/metric.Reader]
diff --git a/exporters/autoexport/signal.go b/exporters/autoexport/signal.go
index 1103a65ae..acb0505b3 100644
--- a/exporters/autoexport/signal.go
+++ b/exporters/autoexport/signal.go
@@ -41,8 +41,8 @@ func (s signal[T]) create(ctx context.Context, opts ...option[T]) (T, error) {
 
 	expType := os.Getenv(s.envKey)
 	if expType == "" {
-		if cfg.hasFallback {
-			return cfg.fallback, nil
+		if cfg.fallbackFactory != nil {
+			return cfg.fallbackFactory(ctx)
 		}
 		expType = "otlp"
 	}
@@ -51,8 +51,7 @@ func (s signal[T]) create(ctx context.Context, opts ...option[T]) (T, error) {
 }
 
 type config[T any] struct {
-	hasFallback bool
-	fallback    T
+	fallbackFactory func(ctx context.Context) (T, error)
 }
 
 type option[T any] interface {
@@ -66,9 +65,8 @@ func (fn optionFunc[T]) apply(cfg *config[T]) {
 	fn(cfg)
 }
 
-func withFallback[T any](fallback T) option[T] {
+func withFallbackFactory[T any](fallbackFactory func(ctx context.Context) (T, error)) option[T] {
 	return optionFunc[T](func(cfg *config[T]) {
-		cfg.hasFallback = true
-		cfg.fallback = fallback
+		cfg.fallbackFactory = fallbackFactory
 	})
 }
diff --git a/exporters/autoexport/spans.go b/exporters/autoexport/spans.go
index af8c1bc30..7d4a5b0cd 100644
--- a/exporters/autoexport/spans.go
+++ b/exporters/autoexport/spans.go
@@ -34,8 +34,8 @@ type Option = SpanOption
 
 // WithFallbackSpanExporter sets the fallback exporter to use when no exporter
 // is configured through the OTEL_TRACES_EXPORTER environment variable.
-func WithFallbackSpanExporter(exporter trace.SpanExporter) SpanOption {
-	return withFallback[trace.SpanExporter](exporter)
+func WithFallbackSpanExporter(spanExporterFactory func(ctx context.Context) (trace.SpanExporter, error)) SpanOption {
+	return withFallbackFactory[trace.SpanExporter](spanExporterFactory)
 }
 
 // NewSpanExporter returns a configured [go.opentelemetry.io/otel/sdk/trace.SpanExporter]


Test patch:
diff --git a/exporters/autoexport/signal_test.go b/exporters/autoexport/signal_test.go
index 10bb6e62e..e4b56308d 100644
--- a/exporters/autoexport/signal_test.go
+++ b/exporters/autoexport/signal_test.go
@@ -16,6 +16,7 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"
 
 import (
 	"context"
+	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -31,10 +32,21 @@ func TestOTLPExporterReturnedWhenNoEnvOrFallbackExporterConfigured(t *testing.T)
 
 func TestFallbackExporterReturnedWhenNoEnvExporterConfigured(t *testing.T) {
 	ts := newSignal[*testType]("TEST_TYPE_KEY")
-	fallback := testType{"test-fallback-exporter"}
-	exp, err := ts.create(context.Background(), withFallback(&fallback))
+	exp, err := ts.create(context.Background(), withFallbackFactory(factory("test-fallback-exporter")))
 	assert.NoError(t, err)
-	assert.Same(t, &fallback, exp)
+	assert.Equal(t, exp.string, "test-fallback-exporter")
+}
+
+func TestFallbackExporterFactoryErrorReturnedWhenNoEnvExporterConfiguredAndFallbackFactoryReturnsAnError(t *testing.T) {
+	ts := newSignal[*testType]("TEST_TYPE_KEY")
+
+	expectedErr := errors.New("error expected to return")
+	errFactory := func(ctx context.Context) (*testType, error) {
+		return nil, expectedErr
+	}
+	exp, err := ts.create(context.Background(), withFallbackFactory(errFactory))
+	assert.ErrorIs(t, err, expectedErr)
+	assert.Nil(t, exp)
 }
 
 func TestEnvExporterIsPreferredOverFallbackExporter(t *testing.T) {
@@ -43,10 +55,9 @@ func TestEnvExporterIsPreferredOverFallbackExporter(t *testing.T) {
 
 	expName := "test-env-exporter-name"
 	t.Setenv(envVariable, expName)
-	fallback := testType{"test-fallback-exporter"}
 	assert.NoError(t, ts.registry.store(expName, factory("test-env-exporter")))
 
-	exp, err := ts.create(context.Background(), withFallback(&fallback))
+	exp, err := ts.create(context.Background(), withFallbackFactory(factory("test-fallback-exporter")))
 	assert.NoError(t, err)
 	assert.Equal(t, exp.string, "test-env-exporter")
 }