Skip to content

Commit 7f882f1

Browse files
committed
feat(exporters/autoexport): enable support of multiple exporters for OTEL_*_EXPORTER (open-telemetry#4471)
This commit introduces the support of comma-separated value for OTEL_{METRICS,TRACES,LOGS}_EXPORTER. New functions can now be used to intialize a list of exporters: NewMetricReaders, NewLogExporters, NewSpanExporters. Old ones (NewMetricReader, NewLogExporter, NewSpanExporter) are now deprecated but still continue to do they initial work to avoid breaking change. Signed-off-by: thomasgouveia <[email protected]>
1 parent 074bc28 commit 7f882f1

File tree

14 files changed

+536
-145
lines changed

14 files changed

+536
-145
lines changed

exporters/autoexport/const.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"
5+
6+
const (
7+
none = "none"
8+
otlp = "otlp"
9+
console = "console"
10+
11+
httpProtobuf = "http/protobuf"
12+
grpc = "grpc"
13+
14+
otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL"
15+
)

exporters/autoexport/factory.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"
5+
6+
import (
7+
"context"
8+
)
9+
10+
// factory is a type alias for a factory method to build a signal-specific exporter.
11+
type factory[T any] func(ctx context.Context) (T, error)
12+
13+
// executor allows different factories to be registered and executed.
14+
type executor[T any] struct {
15+
// factories holds a list of exporter factory functions.
16+
factories []factory[T]
17+
}
18+
19+
func newExecutor[T any]() *executor[T] {
20+
return &executor[T]{
21+
factories: make([]factory[T], 0),
22+
}
23+
}
24+
25+
// Append appends the given factory to the executor.
26+
func (f *executor[T]) Append(fact factory[T]) {
27+
f.factories = append(f.factories, fact)
28+
}
29+
30+
// Execute executes all the factories and returns the results.
31+
// An error will be returned if at least one factory fails.
32+
func (f *executor[T]) Execute(ctx context.Context) ([]T, error) {
33+
var results []T
34+
35+
for _, registered := range f.factories {
36+
result, err := registered(ctx)
37+
if err != nil {
38+
return nil, err
39+
}
40+
results = append(results, result)
41+
}
42+
43+
return results, nil
44+
}

exporters/autoexport/logs.go

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,39 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"
55

66
import (
77
"context"
8-
"os"
8+
"errors"
99

10+
"go.opentelemetry.io/contrib/exporters/autoexport/utils/env"
11+
"go.opentelemetry.io/contrib/exporters/autoexport/utils/functional"
1012
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
1113
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
1214
"go.opentelemetry.io/otel/sdk/log"
1315
)
1416

15-
// LogOption applies an autoexport configuration option.
16-
type LogOption = option[log.Exporter]
17+
const (
18+
otelLogsExporterEnvKey = "OTEL_LOGS_EXPORTER"
19+
otelLogsExporterProtocolEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
20+
)
1721

18-
var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER")
22+
var (
23+
logsSignal = newSignal[log.Exporter](otelLogsExporterEnvKey)
1924

20-
// NewLogExporter returns a configured [go.opentelemetry.io/otel/sdk/log.Exporter]
25+
errLogsUnsupportedGRPCProtocol = errors.New("log exporter do not support 'grpc' protocol yet - consider using 'http/protobuf' instead")
26+
)
27+
28+
// LogExporterOption applies an autoexport configuration option.
29+
type LogExporterOption = functional.Option[config[log.Exporter]]
30+
31+
// WithFallbackLogExporter sets the fallback exporter to use when no exporter
32+
// is configured through the OTEL_LOGS_EXPORTER environment variable.
33+
func WithFallbackLogExporter(factoryFn factory[log.Exporter]) LogExporterOption {
34+
return withFallbackFactory(factoryFn)
35+
}
36+
37+
// NewLogExporters returns one or more configured [go.opentelemetry.io/otel/sdk/log.Exporter]
2138
// defined using the environment variables described below.
2239
//
23-
// OTEL_LOGS_EXPORTER defines the logs exporter; supported values:
40+
// OTEL_LOGS_EXPORTER defines the logs exporter; this value accepts a comma-separated list of values to enable multiple exporters; supported values:
2441
// - "none" - "no operation" exporter
2542
// - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlplog]
2643
// - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdoutlog]
@@ -31,45 +48,80 @@ var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER")
3148
// see: [go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp]
3249
//
3350
// An error is returned if an environment value is set to an unhandled value.
51+
// Use [WithFallbackLogExporter] option to change the returned exporter
52+
// when OTEL_LOGS_EXPORTER is unset or empty.
3453
//
3554
// Use [RegisterLogExporter] to handle more values of OTEL_LOGS_EXPORTER.
3655
//
56+
// Use [IsNoneLogExporter] to check if the returned exporter is a "no operation" exporter.
57+
func NewLogExporters(ctx context.Context, options ...LogExporterOption) ([]log.Exporter, error) {
58+
return logsSignal.create(ctx, options...)
59+
}
60+
61+
// NewLogExporter returns a configured [go.opentelemetry.io/otel/sdk/log.Exporter]
62+
// defined using the environment variables described below.
63+
//
64+
// DEPRECATED: consider using [NewLogExporters] instead.
65+
//
66+
// OTEL_LOGS_EXPORTER defines the logs exporter; supported values:
67+
// - "none" - "no operation" exporter
68+
// - "otlp" (default) - OTLP exporter; see [go.opentelemetry.io/otel/exporters/otlp/otlplog]
69+
// - "console" - Standard output exporter; see [go.opentelemetry.io/otel/exporters/stdout/stdoutlog]
70+
//
71+
// OTEL_EXPORTER_OTLP_PROTOCOL defines OTLP exporter's transport protocol;
72+
// supported values:
73+
// - "http/protobuf" (default) - protobuf-encoded data over HTTP connection;
74+
// see: [go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp]
75+
//
76+
// An error is returned if an environment value is set to an unhandled value.
3777
// Use [WithFallbackLogExporter] option to change the returned exporter
3878
// when OTEL_LOGS_EXPORTER is unset or empty.
3979
//
80+
// Use [RegisterLogExporter] to handle more values of OTEL_LOGS_EXPORTER.
81+
//
4082
// Use [IsNoneLogExporter] to check if the returned exporter is a "no operation" exporter.
41-
func NewLogExporter(ctx context.Context, opts ...LogOption) (log.Exporter, error) {
42-
return logsSignal.create(ctx, opts...)
83+
func NewLogExporter(ctx context.Context, options ...LogExporterOption) (log.Exporter, error) {
84+
exporters, err := NewLogExporters(ctx, options...)
85+
if err != nil {
86+
return nil, err
87+
}
88+
return exporters[0], nil
4389
}
4490

4591
// RegisterLogExporter sets the log.Exporter factory to be used when the
4692
// OTEL_LOGS_EXPORTER environment variable contains the exporter name.
4793
// This will panic if name has already been registered.
48-
func RegisterLogExporter(name string, factory func(context.Context) (log.Exporter, error)) {
49-
must(logsSignal.registry.store(name, factory))
94+
func RegisterLogExporter(name string, factoryFn factory[log.Exporter]) {
95+
must(logsSignal.registry.store(name, factoryFn))
5096
}
5197

5298
func init() {
53-
RegisterLogExporter("otlp", func(ctx context.Context) (log.Exporter, error) {
54-
proto := os.Getenv(otelExporterOTLPProtoEnvKey)
55-
if proto == "" {
56-
proto = "http/protobuf"
57-
}
99+
RegisterLogExporter(otlp, func(ctx context.Context) (log.Exporter, error) {
100+
// The transport protocol used by the exporter is determined using the
101+
// following environment variables, ordered by priority:
102+
// - OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
103+
// - OTEL_EXPORTER_OTLP_PROTOCOL
104+
// - fallback to 'http/protobuf' if variables above are not set or empty.
105+
proto := env.WithDefaultString(
106+
otelLogsExporterProtocolEnvKey,
107+
env.WithDefaultString(otelExporterOTLPProtoEnvKey, httpProtobuf),
108+
)
58109

59110
switch proto {
60-
// grpc is not supported yet, should comment out when it is supported
61-
// case "grpc":
62-
// return otlploggrpc.New(ctx)
63-
case "http/protobuf":
111+
case grpc:
112+
// grpc is not supported yet, should uncomment when it is supported.
113+
// return otlplogrpc.New(ctx)
114+
return nil, errLogsUnsupportedGRPCProtocol
115+
case httpProtobuf:
64116
return otlploghttp.New(ctx)
65117
default:
66118
return nil, errInvalidOTLPProtocol
67119
}
68120
})
69-
RegisterLogExporter("console", func(ctx context.Context) (log.Exporter, error) {
121+
RegisterLogExporter(console, func(_ context.Context) (log.Exporter, error) {
70122
return stdoutlog.New()
71123
})
72-
RegisterLogExporter("none", func(ctx context.Context) (log.Exporter, error) {
124+
RegisterLogExporter(none, func(_ context.Context) (log.Exporter, error) {
73125
return noopLogExporter{}, nil
74126
})
75127
}

exporters/autoexport/logs_test.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"reflect"
1010
"testing"
1111

12+
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
13+
1214
"github.com/stretchr/testify/assert"
1315

1416
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
@@ -17,8 +19,9 @@ import (
1719

1820
func TestLogExporterNone(t *testing.T) {
1921
t.Setenv("OTEL_LOGS_EXPORTER", "none")
20-
got, err := NewLogExporter(context.Background())
22+
exporters, err := NewLogExporters(context.Background())
2123
assert.NoError(t, err)
24+
got := exporters[0]
2225
t.Cleanup(func() {
2326
assert.NoError(t, got.ForceFlush(context.Background()))
2427
assert.NoError(t, got.Shutdown(context.Background()))
@@ -29,8 +32,10 @@ func TestLogExporterNone(t *testing.T) {
2932

3033
func TestLogExporterConsole(t *testing.T) {
3134
t.Setenv("OTEL_LOGS_EXPORTER", "console")
32-
got, err := NewLogExporter(context.Background())
35+
exporters, err := NewLogExporters(context.Background())
3336
assert.NoError(t, err)
37+
38+
got := exporters[0]
3439
assert.IsType(t, &stdoutlog.Exporter{}, got)
3540
}
3641

@@ -46,8 +51,9 @@ func TestLogExporterOTLP(t *testing.T) {
4651
t.Run(fmt.Sprintf("protocol=%q", tc.protocol), func(t *testing.T) {
4752
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tc.protocol)
4853

49-
got, err := NewLogExporter(context.Background())
54+
exporters, err := NewLogExporters(context.Background())
5055
assert.NoError(t, err)
56+
got := exporters[0]
5157
t.Cleanup(func() {
5258
assert.NoError(t, got.Shutdown(context.Background()))
5359
})
@@ -60,10 +66,46 @@ func TestLogExporterOTLP(t *testing.T) {
6066
}
6167
}
6268

69+
func TestLogExporterOTLPMultiple(t *testing.T) {
70+
t.Setenv("OTEL_LOGS_EXPORTER", "otlp,console")
71+
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
72+
73+
exporters, err := NewLogExporters(context.Background())
74+
assert.NoError(t, err)
75+
assert.Len(t, exporters, 2)
76+
77+
assert.Implements(t, new(log.Exporter), exporters[0])
78+
assert.IsType(t, &otlploghttp.Exporter{}, exporters[0])
79+
80+
assert.Implements(t, new(log.Exporter), exporters[1])
81+
assert.IsType(t, &stdoutlog.Exporter{}, exporters[1])
82+
83+
t.Cleanup(func() {
84+
assert.NoError(t, exporters[0].Shutdown(context.Background()))
85+
assert.NoError(t, exporters[1].Shutdown(context.Background()))
86+
})
87+
}
88+
89+
func TestLogExporterOTLPMultiple_FailsIfOneValueIsInvalid(t *testing.T) {
90+
t.Setenv("OTEL_LOGS_EXPORTER", "otlp,something")
91+
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
92+
93+
_, err := NewLogExporters(context.Background())
94+
assert.Error(t, err)
95+
}
96+
6397
func TestLogExporterOTLPOverInvalidProtocol(t *testing.T) {
6498
t.Setenv("OTEL_LOGS_EXPORTER", "otlp")
6599
t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "invalid-protocol")
66100

67-
_, err := NewLogExporter(context.Background())
101+
_, err := NewLogExporters(context.Background())
68102
assert.Error(t, err)
69103
}
104+
105+
func TestLogExporterDeprecatedNewLogExporterReturnsTheFirstExporter(t *testing.T) {
106+
t.Setenv("OTEL_LOGS_EXPORTER", "console,otlp")
107+
got, err := NewLogExporter(context.Background())
108+
109+
assert.NoError(t, err)
110+
assert.IsType(t, &stdoutlog.Exporter{}, got)
111+
}

0 commit comments

Comments
 (0)