Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exporters/autoexport: add support for comma-separated values for OTEL_{METRICS,TRACES,LOGS}_EXPORTER #5830

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions exporters/autoexport/example_test.go
Original file line number Diff line number Diff line change
@@ -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...)
}
41 changes: 41 additions & 0 deletions exporters/autoexport/factory.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 57 additions & 13 deletions exporters/autoexport/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,63 @@

import (
"context"
"errors"
"os"

"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"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)

Check warning on line 33 in exporters/autoexport/logs.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/logs.go#L32-L33

Added lines #L32 - L33 were not covered by tests
}

// 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]
Expand All @@ -36,15 +76,18 @@
// 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

Check warning on line 88 in exporters/autoexport/logs.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/logs.go#L88

Added line #L88 was not covered by tests
}
return exporters[0], nil
}

// RegisterLogExporter sets the log.Exporter factory to be used when the
Expand All @@ -56,7 +99,7 @@

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)
}
Expand All @@ -67,19 +110,20 @@
}

switch proto {
// grpc is not supported yet, should comment out when it is supported
// case "grpc":
// return otlploggrpc.New(ctx)
case "grpc":

Check warning on line 113 in exporters/autoexport/logs.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/logs.go#L113

Added line #L113 was not covered by tests
// grpc is not supported yet, should uncomment when it is supported.
// return otlplogrpc.New(ctx)
return nil, errLogsUnsupportedGRPCProtocol

Check warning on line 116 in exporters/autoexport/logs.go

View check run for this annotation

Codecov / codecov/patch

exporters/autoexport/logs.go#L116

Added line #L116 was not covered by tests
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
})
}
53 changes: 48 additions & 5 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 @@ -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()))
Expand All @@ -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)
}
Loading