diff --git a/exporters/autoexport/const.go b/exporters/autoexport/const.go new file mode 100644 index 00000000000..c8e9a26d5de --- /dev/null +++ b/exporters/autoexport/const.go @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport + +const ( + none = "none" + otlp = "otlp" + console = "console" + + httpProtobuf = "http/protobuf" + grpc = "grpc" + + otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL" +) diff --git a/exporters/autoexport/factory.go b/exporters/autoexport/factory.go new file mode 100644 index 00000000000..0be6f0ed07c --- /dev/null +++ b/exporters/autoexport/factory.go @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport + +import ( + "context" +) + +// factory is a type alias for a factory method to build a signal-specific exporter. +type factory[T any] func(ctx context.Context) (T, error) + +// executor allows different factories to be registered and executed. +type executor[T any] struct { + // factories holds a list of exporter factory functions. + factories []factory[T] +} + +func newExecutor[T any]() *executor[T] { + return &executor[T]{ + factories: make([]factory[T], 0), + } +} + +// Append appends the given factory to the executor. +func (f *executor[T]) Append(fact factory[T]) { + f.factories = append(f.factories, fact) +} + +// Execute executes all the factories and returns the results. +// An error will be returned if at least one factory fails. +func (f *executor[T]) Execute(ctx context.Context) ([]T, error) { + var results []T + + for _, registered := range f.factories { + result, err := registered(ctx) + if err != nil { + return nil, err + } + results = append(results, result) + } + + return results, nil +} diff --git a/exporters/autoexport/go.mod b/exporters/autoexport/go.mod index 2732dcc4fff..439efa7ad97 100644 --- a/exporters/autoexport/go.mod +++ b/exporters/autoexport/go.mod @@ -24,6 +24,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.27.0 go.opentelemetry.io/proto/otlp v1.3.1 go.uber.org/goleak v1.3.0 + golang.org/x/sync v0.7.0 google.golang.org/protobuf v1.34.2 ) diff --git a/exporters/autoexport/go.sum b/exporters/autoexport/go.sum index b8331ee6029..b785a7694df 100644 --- a/exporters/autoexport/go.sum +++ b/exporters/autoexport/go.sum @@ -75,6 +75,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/exporters/autoexport/logs.go b/exporters/autoexport/logs.go index e066a74649e..325ca4172df 100644 --- a/exporters/autoexport/logs.go +++ b/exporters/autoexport/logs.go @@ -5,22 +5,39 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" - "os" + "errors" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/functional" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/sdk/log" ) -// LogOption applies an autoexport configuration option. -type LogOption = option[log.Exporter] +const ( + otelLogsExporterEnvKey = "OTEL_LOGS_EXPORTER" + otelLogsExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" +) -var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER") +var ( + logsSignal = newSignal[log.Exporter](otelLogsExporterEnvKey) -// NewLogExporter returns a configured [go.opentelemetry.io/otel/sdk/log.Exporter] + errLogsUnsupportedGRPCProtocol = errors.New("log exporter do not support 'grpc' protocol yet - consider using 'http/protobuf' instead") +) + +// LogExporterOption applies an autoexport configuration option. +type LogExporterOption = functional.Option[config[log.Exporter]] + +// WithFallbackLogExporter sets the fallback exporter to use when no exporter +// is configured through the OTEL_LOGS_EXPORTER environment variable. +func WithFallbackLogExporter(factoryFn factory[log.Exporter]) LogExporterOption { + return withFallbackFactory(factoryFn) +} + +// NewLogExporters returns one or more configured [go.opentelemetry.io/otel/sdk/log.Exporter] // defined using the environment variables described below. // -// OTEL_LOGS_EXPORTER defines the logs exporter; supported values: +// OTEL_LOGS_EXPORTER defines the logs exporter; this value accepts a comma-separated list of values to enable multiple exporters; supported values: // - "none" - "no operation" exporter // - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlplog] // - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdoutlog] @@ -31,45 +48,80 @@ var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER") // see: [go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp] // // An error is returned if an environment value is set to an unhandled value. +// Use [WithFallbackLogExporter] option to change the returned exporter +// when OTEL_LOGS_EXPORTER is unset or empty. // // Use [RegisterLogExporter] to handle more values of OTEL_LOGS_EXPORTER. // +// Use [IsNoneLogExporter] to check if the returned exporter is a "no operation" exporter. +func NewLogExporters(ctx context.Context, options ...LogExporterOption) ([]log.Exporter, error) { + return logsSignal.create(ctx, options...) +} + +// NewLogExporter returns a configured [go.opentelemetry.io/otel/sdk/log.Exporter] +// defined using the environment variables described below. +// +// DEPRECATED: consider using [NewLogExporters] instead. +// +// OTEL_LOGS_EXPORTER defines the logs exporter; supported values: +// - "none" - "no operation" exporter +// - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlplog] +// - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdoutlog] +// +// OTEL_EXPORTER_OTLP_PROTOCOL defines OTLP exporter's transport protocol; +// supported values: +// - "http/protobuf" (default) - protobuf-encoded data over HTTP connection; +// see: [go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp] +// +// An error is returned if an environment value is set to an unhandled value. // Use [WithFallbackLogExporter] option to change the returned exporter // when OTEL_LOGS_EXPORTER is unset or empty. // +// Use [RegisterLogExporter] to handle more values of OTEL_LOGS_EXPORTER. +// // Use [IsNoneLogExporter] to check if the returned exporter is a "no operation" exporter. -func NewLogExporter(ctx context.Context, opts ...LogOption) (log.Exporter, error) { - return logsSignal.create(ctx, opts...) +func NewLogExporter(ctx context.Context, options ...LogExporterOption) (log.Exporter, error) { + exporters, err := NewLogExporters(ctx, options...) + if err != nil { + return nil, err + } + return exporters[0], nil } // RegisterLogExporter sets the log.Exporter factory to be used when the // OTEL_LOGS_EXPORTER environment variable contains the exporter name. // This will panic if name has already been registered. -func RegisterLogExporter(name string, factory func(context.Context) (log.Exporter, error)) { - must(logsSignal.registry.store(name, factory)) +func RegisterLogExporter(name string, factoryFn factory[log.Exporter]) { + must(logsSignal.registry.store(name, factoryFn)) } func init() { - RegisterLogExporter("otlp", func(ctx context.Context) (log.Exporter, error) { - proto := os.Getenv(otelExporterOTLPProtoEnvKey) - if proto == "" { - proto = "http/protobuf" - } + RegisterLogExporter(otlp, func(ctx context.Context) (log.Exporter, error) { + // The transport protocol used by the exporter is determined using the + // following environment variables, ordered by priority: + // - OTEL_EXPORTER_OTLP_LOGS_PROTOCOL + // - OTEL_EXPORTER_OTLP_PROTOCOL + // - fallback to 'http/protobuf' if variables above are not set or empty. + proto := env.WithDefaultString( + otelLogsExporterProtocolEnvKey, + env.WithDefaultString(otelExporterOTLPProtoEnvKey, httpProtobuf), + ) switch proto { - // grpc is not supported yet, should comment out when it is supported - // case "grpc": - // return otlploggrpc.New(ctx) - case "http/protobuf": + case grpc: + // grpc is not supported yet, should uncomment when it is supported. + // return otlplogrpc.New(ctx) + return nil, errLogsUnsupportedGRPCProtocol + case httpProtobuf: return otlploghttp.New(ctx) default: return nil, errInvalidOTLPProtocol } }) - RegisterLogExporter("console", func(ctx context.Context) (log.Exporter, error) { + RegisterLogExporter(console, func(_ context.Context) (log.Exporter, error) { return stdoutlog.New() }) - RegisterLogExporter("none", func(ctx context.Context) (log.Exporter, error) { + RegisterLogExporter(none, func(_ context.Context) (log.Exporter, error) { return noopLogExporter{}, nil }) } diff --git a/exporters/autoexport/logs_test.go b/exporters/autoexport/logs_test.go index 889a9ea3a82..48951fb4966 100644 --- a/exporters/autoexport/logs_test.go +++ b/exporters/autoexport/logs_test.go @@ -6,6 +6,7 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" "fmt" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "reflect" "testing" @@ -17,8 +18,9 @@ import ( func TestLogExporterNone(t *testing.T) { t.Setenv("OTEL_LOGS_EXPORTER", "none") - got, err := NewLogExporter(context.Background()) + exporters, err := NewLogExporters(context.Background()) assert.NoError(t, err) + got := exporters[0] t.Cleanup(func() { assert.NoError(t, got.ForceFlush(context.Background())) assert.NoError(t, got.Shutdown(context.Background())) @@ -29,8 +31,10 @@ func TestLogExporterNone(t *testing.T) { func TestLogExporterConsole(t *testing.T) { t.Setenv("OTEL_LOGS_EXPORTER", "console") - got, err := NewLogExporter(context.Background()) + exporters, err := NewLogExporters(context.Background()) assert.NoError(t, err) + + got := exporters[0] assert.IsType(t, &stdoutlog.Exporter{}, got) } @@ -46,8 +50,9 @@ func TestLogExporterOTLP(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tc.protocol) - got, err := NewLogExporter(context.Background()) + exporters, err := NewLogExporters(context.Background()) assert.NoError(t, err) + got := exporters[0] t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) }) @@ -60,10 +65,46 @@ func TestLogExporterOTLP(t *testing.T) { } } +func TestLogExporterOTLPMultiple(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "otlp,console") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + exporters, err := NewLogExporters(context.Background()) + assert.NoError(t, err) + assert.Len(t, exporters, 2) + + assert.Implements(t, new(log.Exporter), exporters[0]) + assert.IsType(t, &otlploghttp.Exporter{}, exporters[0]) + + assert.Implements(t, new(log.Exporter), exporters[1]) + assert.IsType(t, &stdoutlog.Exporter{}, exporters[1]) + + t.Cleanup(func() { + assert.NoError(t, exporters[0].Shutdown(context.Background())) + assert.NoError(t, exporters[1].Shutdown(context.Background())) + }) +} + +func TestLogExporterOTLPMultiple_FailsIfOneValueIsInvalid(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "otlp,something") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + _, err := NewLogExporters(context.Background()) + assert.Error(t, err) +} + func TestLogExporterOTLPOverInvalidProtocol(t *testing.T) { t.Setenv("OTEL_LOGS_EXPORTER", "otlp") t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "invalid-protocol") - _, err := NewLogExporter(context.Background()) + _, err := NewLogExporters(context.Background()) assert.Error(t, err) } + +func TestLogExporterDeprecatedNewLogExporterReturnsTheFirstExporter(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "console,otlp") + got, err := NewLogExporter(context.Background()) + + assert.NoError(t, err) + assert.IsType(t, &stdoutlog.Exporter{}, got) +} diff --git a/exporters/autoexport/metrics.go b/exporters/autoexport/metrics.go index 5e16b170b4e..92fe113776e 100644 --- a/exporters/autoexport/metrics.go +++ b/exporters/autoexport/metrics.go @@ -7,6 +7,8 @@ import ( "context" "errors" "fmt" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/functional" "net" "net/http" "os" @@ -25,18 +27,66 @@ import ( "go.opentelemetry.io/otel/sdk/metric" ) -// MetricOption applies an autoexport configuration option. -type MetricOption = option[metric.Reader] +const ( + otelMetricsExporterEnvKey = "OTEL_METRICS_EXPORTER" + otelMetricsProducerEnvKey = "OTEL_METRICS_PRODUCERS" + otelMetricsExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" +) + +var ( + metricsSignal = newSignal[metric.Reader](otelMetricsExporterEnvKey) + metricsProducers = newProducerRegistry(otelMetricsProducerEnvKey) +) + +// MetricReaderOption applies an autoexport configuration option. +type MetricReaderOption = functional.Option[config[metric.Reader]] // WithFallbackMetricReader sets the fallback exporter to use when no exporter // is configured through the OTEL_METRICS_EXPORTER environment variable. -func WithFallbackMetricReader(metricReaderFactory func(ctx context.Context) (metric.Reader, error)) MetricOption { - return withFallbackFactory[metric.Reader](metricReaderFactory) +func WithFallbackMetricReader(metricReaderFactory factory[metric.Reader]) MetricReaderOption { + return withFallbackFactory(metricReaderFactory) +} + +// NewMetricReaders returns one or more configured [go.opentelemetry.io/otel/sdk/metric.Reader] +// defined using the environment variables described below. +// +// OTEL_METRICS_EXPORTER defines the metrics exporter; this value accepts a comma-separated list of values to enable multiple exporters; supported values: +// - "none" - "no operation" exporter +// - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlpmetric] +// - "prometheus" - Prometheus exporter + HTTP server; see [go.opentelemetry.io/otel/exporters/prometheus] +// - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdoutmetric] +// +// OTEL_EXPORTER_OTLP_PROTOCOL defines OTLP exporter's transport protocol; +// supported values: +// - "grpc" - protobuf-encoded data using gRPC wire format over HTTP/2 connection; +// see: [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc] +// - "http/protobuf" (default) - protobuf-encoded data over HTTP connection; +// see: [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp] +// +// OTEL_EXPORTER_PROMETHEUS_HOST (defaulting to "localhost") and +// OTEL_EXPORTER_PROMETHEUS_PORT (defaulting to 9464) define the host and port for the +// Prometheus exporter's HTTP server. +// +// Experimental: OTEL_METRICS_PRODUCERS can be used to configure metric producers. +// supported values: prometheus, none. Multiple values can be specified separated by commas. +// +// An error is returned if an environment value is set to an unhandled value. +// Use [WithFallbackMetricReader] option to change the returned exporter +// when OTEL_METRICS_EXPORTER is unset or empty. +// +// Use [RegisterMetricReader] to handle more values of OTEL_METRICS_EXPORTER. +// Use [RegisterMetricProducer] to handle more values of OTEL_METRICS_PRODUCERS. +// +// Use [IsNoneMetricReader] to check if the returned exporter is a "no operation" exporter. +func NewMetricReaders(ctx context.Context, options ...MetricReaderOption) ([]metric.Reader, error) { + return metricsSignal.create(ctx, options...) } // NewMetricReader returns a configured [go.opentelemetry.io/otel/sdk/metric.Reader] // defined using the environment variables described below. // +// DEPRECATED: consider using [NewMetricReaders] instead. +// // OTEL_METRICS_EXPORTER defines the metrics exporter; supported values: // - "none" - "no operation" exporter // - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlpmetric] @@ -66,14 +116,18 @@ func WithFallbackMetricReader(metricReaderFactory func(ctx context.Context) (met // when OTEL_METRICS_EXPORTER is unset or empty. // // Use [IsNoneMetricReader] to check if the returned exporter is a "no operation" exporter. -func NewMetricReader(ctx context.Context, opts ...MetricOption) (metric.Reader, error) { - return metricsSignal.create(ctx, opts...) +func NewMetricReader(ctx context.Context, options ...MetricReaderOption) (metric.Reader, error) { + readers, err := NewMetricReaders(ctx, options...) + if err != nil { + return nil, err + } + return readers[0], nil } // RegisterMetricReader sets the MetricReader factory to be used when the // OTEL_METRICS_EXPORTERS environment variable contains the exporter name. This // will panic if name has already been registered. -func RegisterMetricReader(name string, factory func(context.Context) (metric.Reader, error)) { +func RegisterMetricReader(name string, factory factory[metric.Reader]) { must(metricsSignal.registry.store(name, factory)) } @@ -90,13 +144,8 @@ func WithFallbackMetricProducer(producerFactory func(ctx context.Context) (metri metricsProducers.fallbackProducer = producerFactory } -var ( - metricsSignal = newSignal[metric.Reader]("OTEL_METRICS_EXPORTER") - metricsProducers = newProducerRegistry("OTEL_METRICS_PRODUCERS") -) - func init() { - RegisterMetricReader("otlp", func(ctx context.Context) (metric.Reader, error) { + RegisterMetricReader(otlp, func(ctx context.Context) (metric.Reader, error) { producers, err := metricsProducers.create(ctx) if err != nil { return nil, err @@ -106,19 +155,19 @@ func init() { readerOpts = append(readerOpts, metric.WithProducer(producer)) } - proto := os.Getenv(otelExporterOTLPProtoEnvKey) - if proto == "" { - proto = "http/protobuf" - } + proto := env.WithDefaultString( + otelMetricsExporterProtocolEnvKey, + env.WithDefaultString(otelExporterOTLPProtoEnvKey, httpProtobuf), + ) switch proto { - case "grpc": + case grpc: r, err := otlpmetricgrpc.New(ctx) if err != nil { return nil, err } return metric.NewPeriodicReader(r, readerOpts...), nil - case "http/protobuf": + case httpProtobuf: r, err := otlpmetrichttp.New(ctx) if err != nil { return nil, err @@ -128,7 +177,7 @@ func init() { return nil, errInvalidOTLPProtocol } }) - RegisterMetricReader("console", func(ctx context.Context) (metric.Reader, error) { + RegisterMetricReader(console, func(ctx context.Context) (metric.Reader, error) { producers, err := metricsProducers.create(ctx) if err != nil { return nil, err @@ -144,7 +193,7 @@ func init() { } return metric.NewPeriodicReader(r, readerOpts...), nil }) - RegisterMetricReader("none", func(ctx context.Context) (metric.Reader, error) { + RegisterMetricReader(none, func(ctx context.Context) (metric.Reader, error) { return newNoopMetricReader(), nil }) RegisterMetricReader("prometheus", func(ctx context.Context) (metric.Reader, error) { @@ -181,8 +230,8 @@ func init() { } // environment variable names and defaults specified at https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#prometheus-exporter - host := getenv("OTEL_EXPORTER_PROMETHEUS_HOST", "localhost") - port := getenv("OTEL_EXPORTER_PROMETHEUS_PORT", "9464") + host := env.WithDefaultString("OTEL_EXPORTER_PROMETHEUS_HOST", "localhost") + port := env.WithDefaultString("OTEL_EXPORTER_PROMETHEUS_PORT", "9464") addr := host + ":" + port lis, err := net.Listen("tcp", addr) if err != nil { @@ -204,7 +253,7 @@ func init() { RegisterMetricProducer("prometheus", func(ctx context.Context) (metric.Producer, error) { return prometheusbridge.NewMetricProducer(), nil }) - RegisterMetricProducer("none", func(ctx context.Context) (metric.Producer, error) { + RegisterMetricProducer(none, func(ctx context.Context) (metric.Producer, error) { return newNoopMetricProducer(), nil }) } @@ -222,14 +271,6 @@ func (rws readerWithServer) Shutdown(ctx context.Context) error { ) } -func getenv(key, fallback string) string { - result, ok := os.LookupEnv(key) - if !ok { - return fallback - } - return result -} - type producerRegistry struct { envKey string fallbackProducer func(context.Context) (metric.Producer, error) @@ -240,7 +281,7 @@ func newProducerRegistry(envKey string) producerRegistry { return producerRegistry{ envKey: envKey, registry: ®istry[metric.Producer]{ - names: make(map[string]func(context.Context) (metric.Producer, error)), + names: make(map[string]factory[metric.Producer]), }, } } @@ -263,7 +304,12 @@ func (pr producerRegistry) create(ctx context.Context) ([]metric.Producer, error producers := dedupedMetricProducers(expType) metricProducers := make([]metric.Producer, 0, len(producers)) for _, producer := range producers { - producer, err := pr.registry.load(ctx, producer) + producerFactory, err := pr.registry.load(producer) + if err != nil { + return nil, err + } + + producer, err := producerFactory(ctx) if err != nil { return nil, err } diff --git a/exporters/autoexport/metrics_test.go b/exporters/autoexport/metrics_test.go index da35b4aa570..eb9b8f3abc5 100644 --- a/exporters/autoexport/metrics_test.go +++ b/exporters/autoexport/metrics_test.go @@ -27,7 +27,8 @@ import ( func TestMetricExporterNone(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "none") - got, err := NewMetricReader(context.Background()) + readers, err := NewMetricReaders(context.Background()) + got := readers[0] assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) @@ -37,7 +38,8 @@ func TestMetricExporterNone(t *testing.T) { func TestMetricExporterConsole(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "console") - got, err := NewMetricReader(context.Background()) + readers, err := NewMetricReaders(context.Background()) + got := readers[0] assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) @@ -60,8 +62,9 @@ func TestMetricExporterOTLP(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tc.protocol) - got, err := NewMetricReader(context.Background()) + readers, err := NewMetricReaders(context.Background()) assert.NoError(t, err) + got := readers[0] t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) }) @@ -74,6 +77,28 @@ func TestMetricExporterOTLP(t *testing.T) { } } +func TestMetricReaderOTLPMultiple(t *testing.T) { + t.Setenv("OTEL_METRICS_EXPORTER", "otlp,console") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + readers, err := NewMetricReaders(context.Background()) + assert.NoError(t, err) + assert.Len(t, readers, 2) + + assert.Implements(t, new(metric.Reader), readers[0]) + exporterType := reflect.Indirect(reflect.ValueOf(readers[0])).FieldByName("exporter").Elem().Type() + assert.Equal(t, "*otlpmetrichttp.Exporter", exporterType.String()) + + assert.Implements(t, new(metric.Reader), readers[1]) + exporterType = reflect.Indirect(reflect.ValueOf(readers[1])).FieldByName("exporter").Elem().Type() + assert.Equal(t, "*stdoutmetric.exporter", exporterType.String()) + + t.Cleanup(func() { + assert.NoError(t, readers[0].Shutdown(context.Background())) + assert.NoError(t, readers[1].Shutdown(context.Background())) + }) +} + func TestMetricExporterOTLPOverInvalidProtocol(t *testing.T) { t.Setenv("OTEL_METRICS_EXPORTER", "otlp") t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "invalid-protocol") @@ -82,6 +107,14 @@ func TestMetricExporterOTLPOverInvalidProtocol(t *testing.T) { assert.Error(t, err) } +func TestMetricReaderOTLPMultiple_FailsIfOneValueIsInvalid(t *testing.T) { + t.Setenv("OTEL_METRICS_EXPORTER", "otlp,something") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + _, err := NewMetricReaders(context.Background()) + assert.Error(t, err) +} + func assertNoOtelHandleErrors(t *testing.T) { h := otel.GetErrorHandler() t.Cleanup(func() { otel.SetErrorHandler(h) }) diff --git a/exporters/autoexport/registry.go b/exporters/autoexport/registry.go index 3d9abcafdc0..3e7c486c5d4 100644 --- a/exporters/autoexport/registry.go +++ b/exporters/autoexport/registry.go @@ -4,20 +4,17 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( - "context" "errors" "fmt" "sync" ) -const otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL" - // registry maintains a map of exporter names to exporter factories // func(context.Context) (T, error) that is safe for concurrent use by multiple // goroutines without additional locking or coordination. type registry[T any] struct { mu sync.Mutex - names map[string]func(context.Context) (T, error) + names map[string]factory[T] } var ( @@ -29,7 +26,7 @@ var ( // the OTEL_EXPORTER_OTLP_PROTOCOL environment variable. errInvalidOTLPProtocol = errors.New("invalid OTLP protocol - should be one of ['grpc', 'http/protobuf']") - // errDuplicateRegistration is returned when an duplicate registration is detected. + // errDuplicateRegistration is returned when a duplicate registration is detected. errDuplicateRegistration = errors.New("duplicate registration") ) @@ -37,26 +34,25 @@ var ( // then execute the factory, returning the created SpanExporter. // errUnknownExporterProducer is returned if the registration is missing and the error from // executing the factory if not nil. -func (r *registry[T]) load(ctx context.Context, key string) (T, error) { +func (r *registry[T]) load(key string) (factory[T], error) { r.mu.Lock() defer r.mu.Unlock() factory, ok := r.names[key] if !ok { - var zero T - return zero, errUnknownExporterProducer + return nil, errUnknownExporterProducer } - return factory(ctx) + return factory, nil } // store sets the factory for a key if is not already in the registry. errDuplicateRegistration // is returned if the registry already contains key. -func (r *registry[T]) store(key string, factory func(context.Context) (T, error)) error { +func (r *registry[T]) store(key string, factoryFn factory[T]) error { r.mu.Lock() defer r.mu.Unlock() if _, ok := r.names[key]; ok { return fmt.Errorf("%w: %q", errDuplicateRegistration, key) } - r.names[key] = factory + r.names[key] = factoryFn return nil } diff --git a/exporters/autoexport/registry_test.go b/exporters/autoexport/registry_test.go index d33b7483c1c..1fa284af787 100644 --- a/exporters/autoexport/registry_test.go +++ b/exporters/autoexport/registry_test.go @@ -16,24 +16,26 @@ import ( type testType struct{ string } -func factory(val string) func(ctx context.Context) (*testType, error) { - return func(ctx context.Context) (*testType, error) { return &testType{val}, nil } +func testFactory(val string) func(ctx context.Context) (*testType, error) { + return func(ctx context.Context) (*testType, error) { + return &testType{val}, nil + } } func newTestRegistry() registry[*testType] { return registry[*testType]{ - names: make(map[string]func(context.Context) (*testType, error)), + names: make(map[string]factory[*testType]), } } func TestCanStoreExporterFactory(t *testing.T) { r := newTestRegistry() - require.NoError(t, r.store("first", factory("first"))) + require.NoError(t, r.store("first", testFactory("first"))) } func TestLoadOfUnknownExporterReturnsError(t *testing.T) { r := newTestRegistry() - exp, err := r.load(context.Background(), "non-existent") + exp, err := r.load("non-existent") assert.Equal(t, err, errUnknownExporterProducer, "empty registry should hold nothing") assert.Nil(t, exp, "non-nil exporter returned") } @@ -42,20 +44,20 @@ func TestRegistryIsConcurrentSafe(t *testing.T) { const exporterName = "stdout" r := newTestRegistry() - require.NoError(t, r.store(exporterName, factory("stdout"))) + require.NoError(t, r.store(exporterName, testFactory("stdout"))) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() - require.ErrorIs(t, r.store(exporterName, factory("stdout")), errDuplicateRegistration) + require.ErrorIs(t, r.store(exporterName, testFactory("stdout")), errDuplicateRegistration) }() wg.Add(1) go func() { defer wg.Done() - _, err := r.load(context.Background(), exporterName) + _, err := r.load(exporterName) assert.NoError(t, err, "missing exporter in registry") }() @@ -66,12 +68,12 @@ func TestSubsequentCallsToGetExporterReturnsNewInstances(t *testing.T) { r := newTestRegistry() const key = "key" - assert.NoError(t, r.store(key, factory(key))) + assert.NoError(t, r.store(key, testFactory(key))) - exp1, err := r.load(context.Background(), key) + exp1, err := r.load(key) assert.NoError(t, err) - exp2, err := r.load(context.Background(), key) + exp2, err := r.load(key) assert.NoError(t, err) assert.NotSame(t, exp1, exp2) @@ -81,10 +83,10 @@ func TestRegistryErrorsOnDuplicateRegisterCalls(t *testing.T) { r := newTestRegistry() const exporterName = "custom" - assert.NoError(t, r.store(exporterName, factory(exporterName))) + assert.NoError(t, r.store(exporterName, testFactory(exporterName))) errString := fmt.Sprintf("%s: %q", errDuplicateRegistration, exporterName) - assert.ErrorContains(t, r.store(exporterName, factory(exporterName)), errString) + assert.ErrorContains(t, r.store(exporterName, testFactory(exporterName)), errString) } func TestMust(t *testing.T) { diff --git a/exporters/autoexport/signal.go b/exporters/autoexport/signal.go index 157e51ff3f9..623868daf68 100644 --- a/exporters/autoexport/signal.go +++ b/exporters/autoexport/signal.go @@ -5,57 +5,59 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" - "os" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/functional" ) +// signal represents a generic OpenTelemetry signal (logs, metrics and traces). type signal[T any] struct { envKey string registry *registry[T] } +// newSignal initializes a new OpenTelemetry signal for the given type T. func newSignal[T any](envKey string) signal[T] { return signal[T]{ envKey: envKey, registry: ®istry[T]{ - names: make(map[string]func(context.Context) (T, error)), + names: make(map[string]factory[T]), }, } } -func (s signal[T]) create(ctx context.Context, opts ...option[T]) (T, error) { - var cfg config[T] - for _, opt := range opts { - opt.apply(&cfg) - } +func (s signal[T]) create(ctx context.Context, options ...functional.Option[config[T]]) ([]T, error) { + cfg, executor := functional.ResolveOptions(options...), newExecutor[T]() - expType := os.Getenv(s.envKey) - if expType == "" { + exporters, err := env.WithStringList(s.envKey, ",") + if err != nil { if cfg.fallbackFactory != nil { - return cfg.fallbackFactory(ctx) + executor.Append(cfg.fallbackFactory) + return executor.Execute(ctx) } - expType = "otlp" + exporters = append(exporters, otlp) } - return s.registry.load(ctx, expType) -} - -type config[T any] struct { - fallbackFactory func(ctx context.Context) (T, error) -} + for _, expType := range exporters { + factory, err := s.registry.load(expType) + if err != nil { + return nil, err + } + executor.Append(factory) + } -type option[T any] interface { - apply(cfg *config[T]) + return executor.Execute(ctx) } -type optionFunc[T any] func(cfg *config[T]) - -//lint:ignore U1000 https://github.com/dominikh/go-tools/issues/1440 -func (fn optionFunc[T]) apply(cfg *config[T]) { - fn(cfg) +// config holds common configuration across the different +// supported signals (logs, traces and metrics). +type config[T any] struct { + fallbackFactory factory[T] } -func withFallbackFactory[T any](fallbackFactory func(ctx context.Context) (T, error)) option[T] { - return optionFunc[T](func(cfg *config[T]) { - cfg.fallbackFactory = fallbackFactory - }) +// withFallbackFactory assigns a fallback factory for the current signal. +func withFallbackFactory[T any](factoryFn factory[T]) functional.Option[config[T]] { + return func(s *config[T]) *config[T] { + s.fallbackFactory = factoryFn + return s + } } diff --git a/exporters/autoexport/signal_test.go b/exporters/autoexport/signal_test.go index f3f65775db5..f6406b82c91 100644 --- a/exporters/autoexport/signal_test.go +++ b/exporters/autoexport/signal_test.go @@ -13,17 +13,17 @@ import ( func TestOTLPExporterReturnedWhenNoEnvOrFallbackExporterConfigured(t *testing.T) { ts := newSignal[*testType]("TEST_TYPE_KEY") - assert.NoError(t, ts.registry.store("otlp", factory("test-otlp-exporter"))) + assert.NoError(t, ts.registry.store("otlp", testFactory("test-otlp-exporter"))) exp, err := ts.create(context.Background()) assert.NoError(t, err) - assert.Equal(t, exp.string, "test-otlp-exporter") + assert.Equal(t, exp[0].string, "test-otlp-exporter") } func TestFallbackExporterReturnedWhenNoEnvExporterConfigured(t *testing.T) { ts := newSignal[*testType]("TEST_TYPE_KEY") - exp, err := ts.create(context.Background(), withFallbackFactory(factory("test-fallback-exporter"))) + exp, err := ts.create(context.Background(), withFallbackFactory(testFactory("test-fallback-exporter"))) assert.NoError(t, err) - assert.Equal(t, exp.string, "test-fallback-exporter") + assert.Equal(t, exp[0].string, "test-fallback-exporter") } func TestFallbackExporterFactoryErrorReturnedWhenNoEnvExporterConfiguredAndFallbackFactoryReturnsAnError(t *testing.T) { @@ -44,9 +44,9 @@ func TestEnvExporterIsPreferredOverFallbackExporter(t *testing.T) { expName := "test-env-exporter-name" t.Setenv(envVariable, expName) - assert.NoError(t, ts.registry.store(expName, factory("test-env-exporter"))) + assert.NoError(t, ts.registry.store(expName, testFactory("test-env-exporter"))) - exp, err := ts.create(context.Background(), withFallbackFactory(factory("test-fallback-exporter"))) + exp, err := ts.create(context.Background(), withFallbackFactory(testFactory("test-fallback-exporter"))) assert.NoError(t, err) - assert.Equal(t, exp.string, "test-env-exporter") + assert.Equal(t, exp[0].string, "test-env-exporter") } diff --git a/exporters/autoexport/spans.go b/exporters/autoexport/spans.go index 0a627ae8be5..ee7dfa3d819 100644 --- a/exporters/autoexport/spans.go +++ b/exporters/autoexport/spans.go @@ -5,31 +5,62 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" - "os" - + "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" + "go.opentelemetry.io/contrib/exporters/autoexport/utils/functional" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/sdk/trace" ) +const ( + otelTracesExporterEnvKey = "OTEL_TRACES_EXPORTER" + otelTracesExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +) + +var ( + tracesSignal = newSignal[trace.SpanExporter](otelTracesExporterEnvKey) +) + // SpanOption applies an autoexport configuration option. -type SpanOption = option[trace.SpanExporter] +type SpanOption = functional.Option[config[trace.SpanExporter]] // Option applies an autoexport configuration option. // // Deprecated: Use SpanOption. type Option = SpanOption -// WithFallbackSpanExporter sets the fallback exporter to use when no exporter -// is configured through the OTEL_TRACES_EXPORTER environment variable. -func WithFallbackSpanExporter(spanExporterFactory func(ctx context.Context) (trace.SpanExporter, error)) SpanOption { - return withFallbackFactory[trace.SpanExporter](spanExporterFactory) +// NewSpanExporters returns one or more configured [go.opentelemetry.io/otel/sdk/trace.SpanExporter] +// defined using the environment variables described below. +// +// OTEL_TRACES_EXPORTER defines the traces exporter; this value accepts a comma-separated list of values; supported values: +// - "none" - "no operation" exporter +// - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlptrace] +// - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdouttrace] +// +// OTEL_EXPORTER_OTLP_PROTOCOL defines OTLP exporter's transport protocol; +// supported values: +// - "grpc" - protobuf-encoded data using gRPC wire format over HTTP/2 connection; +// see: [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc] +// - "http/protobuf" (default) - protobuf-encoded data over HTTP connection; +// see: [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp] +// +// An error is returned if an environment value is set to an unhandled value. +// Use [WithFallbackSpanExporter] option to change the returned exporter +// when OTEL_TRACES_EXPORTER is unset or empty. +// +// Use [RegisterSpanExporter] to handle more values of OTEL_TRACES_EXPORTER. +// +// Use [IsNoneSpanExporter] to check if the returned exporter is a "no operation" exporter. +func NewSpanExporters(ctx context.Context, options ...SpanOption) ([]trace.SpanExporter, error) { + return tracesSignal.create(ctx, options...) } // NewSpanExporter returns a configured [go.opentelemetry.io/otel/sdk/trace.SpanExporter] // defined using the environment variables described below. // +// DEPRECATED: consider using [NewSpanExporters] instead. +// // OTEL_TRACES_EXPORTER defines the traces exporter; supported values: // - "none" - "no operation" exporter // - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlptrace] @@ -43,15 +74,18 @@ func WithFallbackSpanExporter(spanExporterFactory func(ctx context.Context) (tra // see: [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp] // // An error is returned if an environment value is set to an unhandled value. -// -// Use [RegisterSpanExporter] to handle more values of OTEL_TRACES_EXPORTER. -// // Use [WithFallbackSpanExporter] option to change the returned exporter // when OTEL_TRACES_EXPORTER is unset or empty. // +// Use [RegisterSpanExporter] to handle more values of OTEL_TRACES_EXPORTER. +// // Use [IsNoneSpanExporter] to check if the returned exporter is a "no operation" exporter. -func NewSpanExporter(ctx context.Context, opts ...SpanOption) (trace.SpanExporter, error) { - return tracesSignal.create(ctx, opts...) +func NewSpanExporter(ctx context.Context, options ...SpanOption) (trace.SpanExporter, error) { + exporters, err := NewSpanExporters(ctx, options...) + if err != nil { + return nil, err + } + return exporters[0], nil } // RegisterSpanExporter sets the SpanExporter factory to be used when the @@ -61,28 +95,37 @@ func RegisterSpanExporter(name string, factory func(context.Context) (trace.Span must(tracesSignal.registry.store(name, factory)) } -var tracesSignal = newSignal[trace.SpanExporter]("OTEL_TRACES_EXPORTER") +// WithFallbackSpanExporter sets the fallback exporter to use when no exporter +// is configured through the OTEL_TRACES_EXPORTER environment variable. +func WithFallbackSpanExporter(spanExporterFactory func(ctx context.Context) (trace.SpanExporter, error)) SpanOption { + return withFallbackFactory[trace.SpanExporter](spanExporterFactory) +} func init() { - RegisterSpanExporter("otlp", func(ctx context.Context) (trace.SpanExporter, error) { - proto := os.Getenv(otelExporterOTLPProtoEnvKey) - if proto == "" { - proto = "http/protobuf" - } + RegisterSpanExporter(otlp, func(ctx context.Context) (trace.SpanExporter, error) { + // The transport protocol used by the exporter is determined using the + // following environment variables, ordered by priority: + // - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL + // - OTEL_EXPORTER_OTLP_PROTOCOL + // - fallback to 'http/protobuf' if variables above are not set or empty. + proto := env.WithDefaultString( + otelTracesExporterProtocolEnvKey, + env.WithDefaultString(otelExporterOTLPProtoEnvKey, httpProtobuf), + ) switch proto { - case "grpc": + case grpc: return otlptracegrpc.New(ctx) - case "http/protobuf": + case httpProtobuf: return otlptracehttp.New(ctx) default: return nil, errInvalidOTLPProtocol } }) - RegisterSpanExporter("console", func(ctx context.Context) (trace.SpanExporter, error) { + RegisterSpanExporter(console, func(_ context.Context) (trace.SpanExporter, error) { return stdouttrace.New() }) - RegisterSpanExporter("none", func(ctx context.Context) (trace.SpanExporter, error) { + RegisterSpanExporter(none, func(_ context.Context) (trace.SpanExporter, error) { return noopSpanExporter{}, nil }) } diff --git a/exporters/autoexport/spans_test.go b/exporters/autoexport/spans_test.go index e411379e0b2..be22916b0d6 100644 --- a/exporters/autoexport/spans_test.go +++ b/exporters/autoexport/spans_test.go @@ -6,6 +6,7 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" "fmt" + "go.opentelemetry.io/otel/sdk/trace" "reflect" "testing" @@ -17,7 +18,8 @@ import ( func TestSpanExporterNone(t *testing.T) { t.Setenv("OTEL_TRACES_EXPORTER", "none") - got, err := NewSpanExporter(context.Background()) + exporters, err := NewSpanExporters(context.Background()) + got := exporters[0] assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) @@ -27,8 +29,10 @@ func TestSpanExporterNone(t *testing.T) { func TestSpanExporterConsole(t *testing.T) { t.Setenv("OTEL_TRACES_EXPORTER", "console") - got, err := NewSpanExporter(context.Background()) + exporters, err := NewSpanExporters(context.Background()) assert.NoError(t, err) + + got := exporters[0] assert.IsType(t, &stdouttrace.Exporter{}, got) } @@ -45,8 +49,9 @@ func TestSpanExporterOTLP(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tc.protocol) - got, err := NewSpanExporter(context.Background()) + exporters, err := NewSpanExporters(context.Background()) assert.NoError(t, err) + got := exporters[0] t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) }) @@ -59,6 +64,34 @@ func TestSpanExporterOTLP(t *testing.T) { } } +func TestSpanExporterOTLPMultiple(t *testing.T) { + t.Setenv("OTEL_TRACES_EXPORTER", "otlp,console") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + exporters, err := NewSpanExporters(context.Background()) + assert.NoError(t, err) + assert.Len(t, exporters, 2) + + assert.Implements(t, new(trace.SpanExporter), exporters[0]) + assert.IsType(t, &otlptrace.Exporter{}, exporters[0]) + + assert.Implements(t, new(trace.SpanExporter), exporters[1]) + assert.IsType(t, &stdouttrace.Exporter{}, exporters[1]) + + t.Cleanup(func() { + assert.NoError(t, exporters[0].Shutdown(context.Background())) + assert.NoError(t, exporters[1].Shutdown(context.Background())) + }) +} + +func TestSpanExporterOTLPMultiple_FailsIfOneValueIsInvalid(t *testing.T) { + t.Setenv("OTEL_TRACES_EXPORTER", "otlp,something") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + + _, err := NewSpanExporters(context.Background()) + assert.Error(t, err) +} + func TestSpanExporterOTLPOverInvalidProtocol(t *testing.T) { t.Setenv("OTEL_TRACES_EXPORTER", "otlp") t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "invalid-protocol") @@ -66,3 +99,11 @@ func TestSpanExporterOTLPOverInvalidProtocol(t *testing.T) { _, err := NewSpanExporter(context.Background()) assert.Error(t, err) } + +func TestSpanExporterDeprecatedNewSpanExporterReturnsTheFirstExporter(t *testing.T) { + t.Setenv("OTEL_TRACES_EXPORTER", "console,otlp") + got, err := NewSpanExporter(context.Background()) + + assert.NoError(t, err) + assert.IsType(t, &stdouttrace.Exporter{}, got) +} diff --git a/exporters/autoexport/utils/env/env.go b/exporters/autoexport/utils/env/env.go new file mode 100644 index 00000000000..50d11365193 --- /dev/null +++ b/exporters/autoexport/utils/env/env.go @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package env + +import ( + "errors" + "os" + "strings" +) + +var ( + ErrUndefinedVariable = errors.New("environment variable is undefined") + ErrEmptyVariable = errors.New("environment variable is empty") +) + +// WithStringList retrieves the value of an environment variable identified by the key +// and split it using the separator to return a list of items. +func WithStringList(key string, separator string) ([]string, error) { + val, err := WithString(key) + if err != nil { + return make([]string, 0), err + } + return strings.Split(val, separator), nil +} + +// WithDefaultString retrieves the value of an environment variable identified by the key. +// If the environment variable is not set or empty, it returns the fallback default string provided. +func WithDefaultString(key string, fallback string) string { + val, err := WithString(key) + if err != nil { + return fallback + } + return val +} + +// WithString retrieves the value of an environment variable identified by the key. +// +// ErrUndefinedVariable is returned if the environment variable lookup fails. +// ErrEmptyVariable is returned if the environment variable is empty. +func WithString(key string) (string, error) { + val, ok := os.LookupEnv(key) + if !ok { + return "", ErrUndefinedVariable + } + + if val == "" { + return "", ErrEmptyVariable + } + + return val, nil +} diff --git a/exporters/autoexport/utils/functional/option.go b/exporters/autoexport/utils/functional/option.go new file mode 100644 index 00000000000..df813b23617 --- /dev/null +++ b/exporters/autoexport/utils/functional/option.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package functional + +// Option is a type alias +type Option[T any] func(*T) *T + +// ResolveOptions applies the given options to a new T instance +// and return it once options has been applied. +func ResolveOptions[T any](opts ...Option[T]) *T { + o := new(T) + for _, opt := range opts { + if opt != nil { + opt(o) + } + } + return o +}