diff --git a/CHANGELOG.md b/CHANGELOG.md index 41521bc3d77..c2a7ee8b319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Support for stdoutlog exporter in `go.opentelemetry.io/contrib/config`. (#5850) +- Support for comma-separated values for the `OTEL_TRACES_EXPORTER`, `OTEL_LOGS_EXPORTER`, `OTEL_METRICS_EXPORTER` environment variables in `go.opentelemetry.io/contrib/exporters/autoexport`. (#5830) - Add macOS ARM64 platform to the compatibility testing suite. (#5868) - The `go.opentelemetry.io/contrib/bridges/otelzap` module. This module provides an OpenTelemetry logging bridge for `go.uber.org/zap`. (#5191) diff --git a/exporters/autoexport/example_test.go b/exporters/autoexport/example_test.go new file mode 100644 index 00000000000..872483d9572 --- /dev/null +++ b/exporters/autoexport/example_test.go @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport_test + +import ( + "context" + "os" + + "go.opentelemetry.io/contrib/exporters/autoexport" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/trace" +) + +func Example_complete() { + ctx := context.Background() + + // Only for demonstration purposes. + _ = os.Setenv("OTEL_LOGS_EXPORTER", "otlp,console") + _ = os.Setenv("OTEL_TRACES_EXPORTER", "otlp") + _ = os.Setenv("OTEL_METRICS_EXPORTER", "otlp") + + // Consider checking errors in your production code. + logExporters, _ := autoexport.NewLogExporters(ctx) + metricReaders, _ := autoexport.NewMetricReaders(ctx) + traceExporters, _ := autoexport.NewSpanExporters(ctx) + + // Now that your exporters and readers are initialized, + // you can simply initialize the different TracerProvider, + // LoggerProvider and MeterProvider. + // https://opentelemetry.io/docs/languages/go/getting-started/#initialize-the-opentelemetry-sdk + + // Traces + var tracerProviderOpts []trace.TracerProviderOption + for _, traceExporter := range traceExporters { + tracerProviderOpts = append(tracerProviderOpts, trace.WithBatcher(traceExporter)) + } + _ = trace.NewTracerProvider(tracerProviderOpts...) + + // Metrics + var meterProviderOpts []metric.Option + for _, metricReader := range metricReaders { + meterProviderOpts = append(meterProviderOpts, metric.WithReader(metricReader)) + } + _ = metric.NewMeterProvider(meterProviderOpts...) + + // Logs + var loggerProviderOpts []log.LoggerProviderOption + for _, logExporter := range logExporters { + loggerProviderOpts = append(loggerProviderOpts, log.WithProcessor( + log.NewBatchProcessor(logExporter), + )) + } + _ = log.NewLoggerProvider(loggerProviderOpts...) +} diff --git a/exporters/autoexport/factory.go b/exporters/autoexport/factory.go new file mode 100644 index 00000000000..0e1f00f3d24 --- /dev/null +++ b/exporters/autoexport/factory.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" + +import ( + "context" +) + +// executor allows different factories to be registered and executed. +type executor[T any] struct { + // factories holds a list of exporter factory functions. + factories []func(ctx context.Context) (T, error) +} + +func newExecutor[T any]() *executor[T] { + return &executor[T]{ + factories: make([]func(ctx context.Context) (T, error), 0), + } +} + +// Append appends the given factory to the executor. +func (f *executor[T]) Append(factory func(ctx context.Context) (T, error)) { + f.factories = append(f.factories, factory) +} + +// 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/logs.go b/exporters/autoexport/logs.go index 9e926ce32e1..88007d317cb 100644 --- a/exporters/autoexport/logs.go +++ b/exporters/autoexport/logs.go @@ -5,6 +5,7 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" + "errors" "os" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" @@ -12,16 +13,55 @@ import ( "go.opentelemetry.io/otel/sdk/log" ) -const otelExporterOTLPLogsProtoEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" +const ( + otelLogsExporterEnvKey = "OTEL_LOGS_EXPORTER" + otelLogsExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" +) + +var ( + logsSignal = newSignal[log.Exporter](otelLogsExporterEnvKey) + + errLogsUnsupportedGRPCProtocol = errors.New("log exporter do not support 'grpc' protocol yet - consider using 'http/protobuf' instead") +) // LogOption applies an autoexport configuration option. type LogOption = option[log.Exporter] -var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER") +// WithFallbackLogExporter sets the fallback exporter to use when no exporter +// is configured through the OTEL_LOGS_EXPORTER environment variable. +func WithFallbackLogExporter(factory func(context.Context) (log.Exporter, error)) LogOption { + return withFallbackFactory(factory) +} + +// 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; 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] +// +// 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 NewLogExporters(ctx context.Context, options ...LogOption) ([]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] @@ -36,15 +76,18 @@ var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER") // supported values are the same as OTEL_EXPORTER_OTLP_PROTOCOL. // // An error is returned if an environment value is set to an unhandled value. -// -// Use [RegisterLogExporter] to handle more values of OTEL_LOGS_EXPORTER. -// // 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 ...LogOption) (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 @@ -56,7 +99,7 @@ func RegisterLogExporter(name string, factory func(context.Context) (log.Exporte func init() { RegisterLogExporter("otlp", func(ctx context.Context) (log.Exporter, error) { - proto := os.Getenv(otelExporterOTLPLogsProtoEnvKey) + proto := os.Getenv(otelLogsExporterProtocolEnvKey) if proto == "" { proto = os.Getenv(otelExporterOTLPProtoEnvKey) } @@ -67,19 +110,20 @@ func init() { } switch proto { - // grpc is not supported yet, should comment out when it is supported - // case "grpc": - // return otlploggrpc.New(ctx) + case "grpc": + // grpc is not supported yet, should uncomment when it is supported. + // return otlplogrpc.New(ctx) + return nil, errLogsUnsupportedGRPCProtocol case "http/protobuf": 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 8b6c2a5a686..b60a80cfba5 100644 --- a/exporters/autoexport/logs_test.go +++ b/exporters/autoexport/logs_test.go @@ -9,6 +9,8 @@ import ( "reflect" "testing" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" + "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" @@ -17,8 +19,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 +32,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 +51,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())) }) @@ -72,7 +78,8 @@ func TestLogExporterOTLPWithDedicatedProtocol(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", tc.protocol) - got, err := NewLogExporter(context.Background()) + exporters, err := NewLogExporters(context.Background()) + got := exporters[0] assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, got.Shutdown(context.Background())) @@ -86,10 +93,46 @@ func TestLogExporterOTLPWithDedicatedProtocol(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 f7543eeed52..467684e24a3 100644 --- a/exporters/autoexport/metrics.go +++ b/exporters/autoexport/metrics.go @@ -25,7 +25,16 @@ import ( "go.opentelemetry.io/otel/sdk/metric" ) -const otelExporterOTLPMetricsProtoEnvKey = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" +const ( + otelMetricsExporterEnvKey = "OTEL_METRICS_EXPORTER" + otelMetricsProducerEnvKey = "OTEL_METRICS_PRODUCERS" + otelMetricsExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" +) + +var ( + metricsSignal = newSignal[metric.Reader](otelMetricsExporterEnvKey) + metricsProducers = newProducerRegistry(otelMetricsProducerEnvKey) +) // MetricOption applies an autoexport configuration option. type MetricOption = option[metric.Reader] @@ -33,12 +42,49 @@ 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(metricReaderFactory func(ctx context.Context) (metric.Reader, error)) MetricOption { - return withFallbackFactory[metric.Reader](metricReaderFactory) + 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 ...MetricOption) ([]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] @@ -72,13 +118,17 @@ func WithFallbackMetricReader(metricReaderFactory func(ctx context.Context) (met // // 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...) + readers, err := NewMetricReaders(ctx, opts...) + 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 func(ctx context.Context) (metric.Reader, error)) { must(metricsSignal.registry.store(name, factory)) } @@ -95,11 +145,6 @@ 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) { producers, err := metricsProducers.create(ctx) @@ -111,7 +156,7 @@ func init() { readerOpts = append(readerOpts, metric.WithProducer(producer)) } - proto := os.Getenv(otelExporterOTLPMetricsProtoEnvKey) + proto := os.Getenv(otelMetricsExporterProtocolEnvKey) if proto == "" { proto = os.Getenv(otelExporterOTLPProtoEnvKey) } @@ -273,7 +318,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 228d2e808c4..4ffefe59e79 100644 --- a/exporters/autoexport/metrics_test.go +++ b/exporters/autoexport/metrics_test.go @@ -28,7 +28,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())) @@ -38,7 +39,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())) @@ -61,8 +63,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())) }) @@ -88,7 +91,8 @@ func TestMetricExporterOTLPWithDedicatedProtocol(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", tc.protocol) - 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())) @@ -102,6 +106,28 @@ func TestMetricExporterOTLPWithDedicatedProtocol(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") @@ -110,6 +136,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..d1ba725014f 100644 --- a/exporters/autoexport/registry.go +++ b/exporters/autoexport/registry.go @@ -29,7 +29,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,15 +37,14 @@ 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) (func(context.Context) (T, error), 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 diff --git a/exporters/autoexport/registry_test.go b/exporters/autoexport/registry_test.go index d33b7483c1c..183fc463bd5 100644 --- a/exporters/autoexport/registry_test.go +++ b/exporters/autoexport/registry_test.go @@ -33,7 +33,7 @@ func TestCanStoreExporterFactory(t *testing.T) { 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") } @@ -55,7 +55,7 @@ func TestRegistryIsConcurrentSafe(t *testing.T) { 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") }() @@ -68,10 +68,10 @@ func TestSubsequentCallsToGetExporterReturnsNewInstances(t *testing.T) { const key = "key" assert.NoError(t, r.store(key, factory(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) diff --git a/exporters/autoexport/signal.go b/exporters/autoexport/signal.go index 157e51ff3f9..afffe2a5a3c 100644 --- a/exporters/autoexport/signal.go +++ b/exporters/autoexport/signal.go @@ -5,14 +5,17 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" - "os" + + "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" ) +// 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, @@ -22,21 +25,32 @@ func newSignal[T any](envKey string) signal[T] { } } -func (s signal[T]) create(ctx context.Context, opts ...option[T]) (T, error) { +func (s signal[T]) create(ctx context.Context, opts ...option[T]) ([]T, error) { var cfg config[T] for _, opt := range opts { opt.apply(&cfg) } - expType := os.Getenv(s.envKey) - if expType == "" { + executor := newExecutor[T]() + + 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) + } + exporters = append(exporters, "otlp") + } + + for _, expType := range exporters { + factory, err := s.registry.load(expType) + if err != nil { + return nil, err } - expType = "otlp" + executor.Append(factory) } - return s.registry.load(ctx, expType) + return executor.Execute(ctx) } type config[T any] struct { diff --git a/exporters/autoexport/signal_test.go b/exporters/autoexport/signal_test.go index f3f65775db5..f0c694761e7 100644 --- a/exporters/autoexport/signal_test.go +++ b/exporters/autoexport/signal_test.go @@ -16,14 +16,14 @@ func TestOTLPExporterReturnedWhenNoEnvOrFallbackExporterConfigured(t *testing.T) assert.NoError(t, ts.registry.store("otlp", factory("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"))) 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) { @@ -48,5 +48,5 @@ func TestEnvExporterIsPreferredOverFallbackExporter(t *testing.T) { exp, err := ts.create(context.Background(), withFallbackFactory(factory("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 8970d18f725..ed7c0ff2a18 100644 --- a/exporters/autoexport/spans.go +++ b/exporters/autoexport/spans.go @@ -13,7 +13,12 @@ import ( "go.opentelemetry.io/otel/sdk/trace" ) -const otelExporterOTLPTracesProtoEnvKey = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +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] @@ -23,15 +28,37 @@ type SpanOption = option[trace.SpanExporter] // 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] @@ -48,15 +75,18 @@ func WithFallbackSpanExporter(spanExporterFactory func(ctx context.Context) (tra // supported values are the same as OTEL_EXPORTER_OTLP_PROTOCOL. // // 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...) + exporters, err := NewSpanExporters(ctx, opts...) + if err != nil { + return nil, err + } + return exporters[0], nil } // RegisterSpanExporter sets the SpanExporter factory to be used when the @@ -66,11 +96,15 @@ 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(otelExporterOTLPTracesProtoEnvKey) + proto := os.Getenv(otelTracesExporterProtocolEnvKey) if proto == "" { proto = os.Getenv(otelExporterOTLPProtoEnvKey) } @@ -89,10 +123,10 @@ func init() { 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 191f955d325..4ba2d8104f3 100644 --- a/exporters/autoexport/spans_test.go +++ b/exporters/autoexport/spans_test.go @@ -9,6 +9,8 @@ import ( "reflect" "testing" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" @@ -17,7 +19,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 +30,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 +50,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())) }) @@ -72,7 +78,8 @@ func TestSpanExporterOTLPWithDedicatedProtocol(t *testing.T) { t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) { t.Setenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", tc.protocol) - 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())) @@ -86,6 +93,34 @@ func TestSpanExporterOTLPWithDedicatedProtocol(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") @@ -93,3 +128,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..2aae1bd8f66 --- /dev/null +++ b/exporters/autoexport/utils/env/env.go @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package env // import "go.opentelemetry.io/contrib/exporters/autoexport/utils/env" + +import ( + "errors" + "os" + "strings" +) + +var ( + // ErrUndefinedVariable is returned when an environment variable is not set. + ErrUndefinedVariable = errors.New("environment variable is undefined") + // ErrEmptyVariable is returned when an environment variable is set but empty. + 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 +} + +// 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 +}