Skip to content

Commit

Permalink
feat(exporters/autoexport): enable support of multiple exporters for …
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
thomasgouveia committed Jun 30, 2024
1 parent 074bc28 commit 7f882f1
Show file tree
Hide file tree
Showing 14 changed files with 536 additions and 145 deletions.
15 changes: 15 additions & 0 deletions exporters/autoexport/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport"

const (
none = "none"
otlp = "otlp"
console = "console"

httpProtobuf = "http/protobuf"
grpc = "grpc"

otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL"
)
44 changes: 44 additions & 0 deletions exporters/autoexport/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package autoexport // import "go.opentelemetry.io/contrib/exporters/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
}
94 changes: 73 additions & 21 deletions exporters/autoexport/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
})
}
50 changes: 46 additions & 4 deletions exporters/autoexport/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()))
Expand All @@ -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)
}

Expand All @@ -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()))
})
Expand All @@ -60,10 +66,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)
}
Loading

0 comments on commit 7f882f1

Please sign in to comment.