diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f99a77d5b..df9723fb9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `go.opentelemetry.io/contrib/config` add support to configure periodic reader interval and timeout. (#5661) - Add support to configure views when creating MeterProvider using the config package. (#5654) +- Add log support for the autoexport package. (#5733) ### Fixed diff --git a/exporters/autoexport/go.mod b/exporters/autoexport/go.mod index 414ab00c684..f1d380f1d03 100644 --- a/exporters/autoexport/go.mod +++ b/exporters/autoexport/go.mod @@ -8,15 +8,18 @@ require ( go.opentelemetry.io/collector/pdata v1.8.0 go.opentelemetry.io/contrib/bridges/prometheus v0.52.0 go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 go.opentelemetry.io/otel/exporters/prometheus v0.49.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.3.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/otel/sdk/log v0.3.0 go.opentelemetry.io/otel/sdk/metric v1.27.0 go.uber.org/goleak v1.3.0 ) @@ -37,6 +40,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.54.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + go.opentelemetry.io/otel/log v0.3.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect diff --git a/exporters/autoexport/go.sum b/exporters/autoexport/go.sum index 0a5abf9920e..f82c059b5fc 100644 --- a/exporters/autoexport/go.sum +++ b/exporters/autoexport/go.sum @@ -54,6 +54,8 @@ go.opentelemetry.io/collector/pdata v1.8.0 h1:d/QQgZxB4Y+d3mqLVh2ozvzujUhloD3P/f go.opentelemetry.io/collector/pdata v1.8.0/go.mod h1:/W7clu0wFC4WSRp94Ucn6Vm36Wkrt+tmtlDb1aiNZCY= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 h1:ccBrA8nCY5mM0y5uO7FT0ze4S0TuFcWdDB2FxGMTjkI= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0/go.mod h1:/9pb6634zi2Lk8LYg9Q0X8Ar6jka4dkFOylBLbVQPCE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= @@ -66,14 +68,20 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.3.0 h1:6aGq6rMOdOx9B385JpF1OpeL18+6Ho8bTFdxy10oEGY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.3.0/go.mod h1:fdZI+pB2Y6Dpl3Uf+1ZPrkX6cnwsUAhjK1f9yCAlJIM= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= +go.opentelemetry.io/otel/log v0.3.0 h1:kJRFkpUFYtny37NQzL386WbznUByZx186DpEMKhEGZs= +go.opentelemetry.io/otel/log v0.3.0/go.mod h1:ziCwqZr9soYDwGNbIL+6kAvQC+ANvjgG367HVcyR/ys= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/log v0.3.0 h1:GEjJ8iftz2l+XO1GF2856r7yYVh74URiF9JMcAacr5U= +go.opentelemetry.io/otel/sdk/log v0.3.0/go.mod h1:BwCxtmux6ACLuys1wlbc0+vGBd+xytjmjajwqqIul2g= go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= diff --git a/exporters/autoexport/logs.go b/exporters/autoexport/logs.go new file mode 100644 index 00000000000..e066a74649e --- /dev/null +++ b/exporters/autoexport/logs.go @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" + +import ( + "context" + "os" + + "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] + +var logsSignal = newSignal[log.Exporter]("OTEL_LOGS_EXPORTER") + +// NewLogExporter returns a configured [go.opentelemetry.io/otel/sdk/log.Exporter] +// defined using the environment variables described below. +// +// 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 [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 [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...) +} + +// 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 init() { + RegisterLogExporter("otlp", func(ctx context.Context) (log.Exporter, error) { + proto := os.Getenv(otelExporterOTLPProtoEnvKey) + if proto == "" { + proto = "http/protobuf" + } + + switch proto { + // grpc is not supported yet, should comment out when it is supported + // case "grpc": + // return otlploggrpc.New(ctx) + case "http/protobuf": + return otlploghttp.New(ctx) + default: + return nil, errInvalidOTLPProtocol + } + }) + RegisterLogExporter("console", func(ctx context.Context) (log.Exporter, error) { + return stdoutlog.New() + }) + RegisterLogExporter("none", func(ctx context.Context) (log.Exporter, error) { + return noopLogExporter{}, nil + }) +} diff --git a/exporters/autoexport/logs_test.go b/exporters/autoexport/logs_test.go new file mode 100644 index 00000000000..889a9ea3a82 --- /dev/null +++ b/exporters/autoexport/logs_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/sdk/log" +) + +func TestLogExporterNone(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "none") + got, err := NewLogExporter(context.Background()) + assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.ForceFlush(context.Background())) + assert.NoError(t, got.Shutdown(context.Background())) + }) + assert.NoError(t, got.Export(context.Background(), nil)) + assert.True(t, IsNoneLogExporter(got)) +} + +func TestLogExporterConsole(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "console") + got, err := NewLogExporter(context.Background()) + assert.NoError(t, err) + assert.IsType(t, &stdoutlog.Exporter{}, got) +} + +func TestLogExporterOTLP(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "otlp") + + for _, tc := range []struct { + protocol, clientType string + }{ + {"http/protobuf", "atomic.Pointer[go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp.client]"}, + {"", "atomic.Pointer[go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp.client]"}, + } { + 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()) + assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, got.Shutdown(context.Background())) + }) + assert.Implements(t, new(log.Exporter), got) + + // Implementation detail hack. This may break when bumping OTLP exporter modules as it uses unexported API. + clientType := reflect.Indirect(reflect.ValueOf(got)).FieldByName("client").Type() + assert.Equal(t, tc.clientType, clientType.String()) + }) + } +} + +func TestLogExporterOTLPOverInvalidProtocol(t *testing.T) { + t.Setenv("OTEL_LOGS_EXPORTER", "otlp") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "invalid-protocol") + + _, err := NewLogExporter(context.Background()) + assert.Error(t, err) +} diff --git a/exporters/autoexport/noop.go b/exporters/autoexport/noop.go index 7ea4bd69754..2a8b173b266 100644 --- a/exporters/autoexport/noop.go +++ b/exporters/autoexport/noop.go @@ -6,6 +6,7 @@ package autoexport // import "go.opentelemetry.io/contrib/exporters/autoexport" import ( "context" + "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/trace" @@ -57,3 +58,30 @@ func (e noopMetricProducer) Produce(ctx context.Context) ([]metricdata.ScopeMetr func newNoopMetricProducer() noopMetricProducer { return noopMetricProducer{} } + +// noopLogExporter is an implementation of log.SpanExporter that performs no operations. +type noopLogExporter struct{} + +var _ log.Exporter = noopLogExporter{} + +// ExportSpans is part of log.Exporter interface. +func (e noopLogExporter) Export(ctx context.Context, records []log.Record) error { + return nil +} + +// Shutdown is part of log.Exporter interface. +func (e noopLogExporter) Shutdown(ctx context.Context) error { + return nil +} + +// ForceFlush is part of log.Exporter interface. +func (e noopLogExporter) ForceFlush(ctx context.Context) error { + return nil +} + +// IsNoneLogExporter returns true for the exporter returned by [NewLogExporter] +// when OTEL_LOGSS_EXPORTER environment variable is set to "none". +func IsNoneLogExporter(e log.Exporter) bool { + _, ok := e.(noopLogExporter) + return ok +}