From 8b23c864d92c7eb9aa1c6d733e5b95791259aab9 Mon Sep 17 00:00:00 2001 From: dmathieu Date: Wed, 3 Apr 2024 09:31:13 +0200 Subject: [PATCH] move logtest into a recorder within the api --- log/logtest/config.go | 41 +++++++++++ log/logtest/recorder.go | 108 +++++++++++++++++++++++++++++ log/logtest/recorder_test.go | 113 +++++++++++++++++++++++++++++++ sdk/log/logtest/README.md | 3 - sdk/log/logtest/exporter.go | 62 ----------------- sdk/log/logtest/exporter_test.go | 63 ----------------- 6 files changed, 262 insertions(+), 128 deletions(-) create mode 100644 log/logtest/config.go create mode 100644 log/logtest/recorder.go create mode 100644 log/logtest/recorder_test.go delete mode 100644 sdk/log/logtest/README.md delete mode 100644 sdk/log/logtest/exporter.go delete mode 100644 sdk/log/logtest/exporter_test.go diff --git a/log/logtest/config.go b/log/logtest/config.go new file mode 100644 index 000000000000..0f25b665acd0 --- /dev/null +++ b/log/logtest/config.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest // import "go.opentelemetry.io/otel/log/logtest" + +import ( + "go.opentelemetry.io/otel/log" +) + +type config struct { + minSeverity log.Severity +} + +func newConfig(options []Option) config { + var c config + for _, opt := range options { + c = opt.apply(c) + } + + return c +} + +// Option configures a [Hook]. +type Option interface { + apply(config) config +} + +type optFunc func(config) config + +func (f optFunc) apply(c config) config { return f(c) } + +// WithMinSeverity returns an [Option] that configures the minimum severity the +// recorder will return true for when Enabled is called. +// +// By default, the recorder will be enabled for all levels. +func WithMinSeverity(l log.Severity) Option { + return optFunc(func(c config) config { + c.minSeverity = l + return c + }) +} diff --git a/log/logtest/recorder.go b/log/logtest/recorder.go new file mode 100644 index 000000000000..ebba208b6a31 --- /dev/null +++ b/log/logtest/recorder.go @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package logtest is a testing helper package. User can retrieve an in-memory +// logger to verify the behavior of their integrations. +package logtest // import "go.opentelemetry.io/otel/log/logtest" + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/embedded" +) + +// embeddedLogger is a type alias so the embedded.Logger type doesn't conflict +// with the Logger method of the recorder when it is embedded. +type embeddedLogger = embedded.Logger // nolint:unused // Used below. + +type enablerKey uint + +var enableKey enablerKey + +// NewInMemoryRecorder returns a new InMemoryRecorder. +func NewInMemoryRecorder(options ...Option) *InMemoryRecorder { + cfg := newConfig(options) + return &InMemoryRecorder{ + minSeverity: cfg.minSeverity, + } +} + +// Scope represents the instrumentation scope. +type Scope struct { + // Name is the name of the instrumentation scope. + Name string + // Version is the version of the instrumentation scope. + Version string + // SchemaURL of the telemetry emitted by the scope. + SchemaURL string +} + +// InMemoryRecorder is a recorder that stores all received log records +// in-memory. +type InMemoryRecorder struct { + embedded.LoggerProvider + embeddedLogger // nolint:unused // Used to embed embedded.Logger. + + mu sync.Mutex + + records []log.Record + + // Scope is the Logger scope recorder received when Logger was called. + Scope Scope + + // minSeverity is the minimum severity the recorder will return true for + // when Enabled is called (unless enableKey is set). + minSeverity log.Severity +} + +// Logger retrieves acopy of InMemoryRecorder with the provided scope +// information. +func (i *InMemoryRecorder) Logger(name string, opts ...log.LoggerOption) log.Logger { + cfg := log.NewLoggerConfig(opts...) + + i.Scope = Scope{ + Name: name, + Version: cfg.InstrumentationVersion(), + SchemaURL: cfg.SchemaURL(), + } + + return i +} + +// Enabled indicates whether a specific record should be stored, according to +// its severity, or context values. +func (i *InMemoryRecorder) Enabled(ctx context.Context, record log.Record) bool { + return ctx.Value(enableKey) != nil || record.Severity() >= i.minSeverity +} + +// Emit stores the log record. +func (i *InMemoryRecorder) Emit(_ context.Context, record log.Record) { + i.mu.Lock() + defer i.mu.Unlock() + + i.records = append(i.records, record) +} + +// GetRecords returns the current in-memory recorder log records. +func (i *InMemoryRecorder) GetRecords() []log.Record { + i.mu.Lock() + defer i.mu.Unlock() + ret := make([]log.Record, len(i.records)) + copy(ret, i.records) + return ret +} + +// Reset the current in-memory recorder log records. +func (i *InMemoryRecorder) Reset() { + i.mu.Lock() + defer i.mu.Unlock() + i.records = []log.Record{} +} + +// ContextWithEnabledRecorder forces enabling the recorder, no matter the log +// severity level. +func ContextWithEnabledRecorder(ctx context.Context) context.Context { + return context.WithValue(ctx, enableKey, true) +} diff --git a/log/logtest/recorder_test.go b/log/logtest/recorder_test.go new file mode 100644 index 000000000000..063ecd5a12e3 --- /dev/null +++ b/log/logtest/recorder_test.go @@ -0,0 +1,113 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestInMemoryRecorderLogger(t *testing.T) { + for _, tt := range []struct { + name string + options []Option + + loggerName string + loggerOptions []log.LoggerOption + + expectedLogger log.Logger + }{ + { + name: "provides a default logger", + + expectedLogger: &InMemoryRecorder{}, + }, + { + name: "provides a logger with a configured scope", + + loggerName: "test", + loggerOptions: []log.LoggerOption{ + log.WithInstrumentationVersion("logtest v42"), + log.WithSchemaURL("https://example.com"), + }, + + expectedLogger: &InMemoryRecorder{ + Scope: Scope{ + Name: "test", + Version: "logtest v42", + SchemaURL: "https://example.com", + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + l := NewInMemoryRecorder(tt.options...).Logger(tt.loggerName, tt.loggerOptions...) + assert.Equal(t, tt.expectedLogger, l) + }) + } +} + +func TestInMemoryRecorderEnabled(t *testing.T) { + for _, tt := range []struct { + name string + options []Option + ctx context.Context + buildRecord func() log.Record + + isEnabled bool + }{ + { + name: "the default option enables unset levels", + ctx: context.Background(), + buildRecord: func() log.Record { + return log.Record{} + }, + + isEnabled: true, + }, + { + name: "with a minimum severity set disables", + options: []Option{ + WithMinSeverity(log.SeverityWarn1), + }, + ctx: context.Background(), + buildRecord: func() log.Record { + return log.Record{} + }, + + isEnabled: false, + }, + { + name: "with a context that forces an enabled recorder", + options: []Option{ + WithMinSeverity(log.SeverityWarn1), + }, + ctx: ContextWithEnabledRecorder(context.Background()), + buildRecord: func() log.Record { + return log.Record{} + }, + + isEnabled: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + e := NewInMemoryRecorder(tt.options...).Enabled(tt.ctx, tt.buildRecord()) + assert.Equal(t, tt.isEnabled, e) + }) + } +} + +func TestInMemoryRecorderEmitAndReset(t *testing.T) { + r := NewInMemoryRecorder() + assert.Len(t, r.GetRecords(), 0) + r.Emit(context.Background(), log.Record{}) + assert.Len(t, r.GetRecords(), 1) + + r.Reset() + assert.Len(t, r.GetRecords(), 0) +} diff --git a/sdk/log/logtest/README.md b/sdk/log/logtest/README.md deleted file mode 100644 index 26ca6d36dcf2..000000000000 --- a/sdk/log/logtest/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# SDK Log test - -[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/sdk/log/logtest)](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/log/logtest) diff --git a/sdk/log/logtest/exporter.go b/sdk/log/logtest/exporter.go deleted file mode 100644 index dede29e75333..000000000000 --- a/sdk/log/logtest/exporter.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package logtest is a testing helper package for the SDK. User can configure -// in-memory exporters to verify different SDK behaviors or custom -// instrumentation. -package logtest // import "go.opentelemetry.io/otel/sdk/log/logtest" - -import ( - "context" - "sync" - - sdklog "go.opentelemetry.io/otel/sdk/log" -) - -// NewInMemoryExporter returns a new InMemoryExporter. -func NewInMemoryExporter() *InMemoryExporter { - return new(InMemoryExporter) -} - -// InMemoryExporter is an exporter that stores all received log records -// in-memory. -type InMemoryExporter struct { - mu sync.Mutex - records []sdklog.Record -} - -// Export handles the export of records by storing them in memory. -func (imsb *InMemoryExporter) Export(ctx context.Context, records []sdklog.Record) error { - imsb.mu.Lock() - defer imsb.mu.Unlock() - - imsb.records = append(imsb.records, records...) - return nil -} - -// Shutdown stops the exporter by clearing the records held in memory. -func (imsb *InMemoryExporter) Shutdown(context.Context) error { - imsb.Reset() - return nil -} - -// ForceFlush is a noop method in the context of InMemoryExporter. -func (imsb *InMemoryExporter) ForceFlush(ctx context.Context) error { - return nil -} - -// Reset the current in-memory storage. -func (imsb *InMemoryExporter) Reset() { - imsb.mu.Lock() - defer imsb.mu.Unlock() - imsb.records = []sdklog.Record{} -} - -// GetRecords returns the current in-memory stored log records. -func (imsb *InMemoryExporter) GetRecords() []sdklog.Record { - imsb.mu.Lock() - defer imsb.mu.Unlock() - ret := make([]sdklog.Record, len(imsb.records)) - copy(ret, imsb.records) - return ret -} diff --git a/sdk/log/logtest/exporter_test.go b/sdk/log/logtest/exporter_test.go deleted file mode 100644 index bf8df67e51fa..000000000000 --- a/sdk/log/logtest/exporter_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "go.opentelemetry.io/otel/log" - sdklog "go.opentelemetry.io/otel/sdk/log" -) - -func TestInMemoryExporterImplementsExporter(t *testing.T) { - assert.Implements(t, (*sdklog.Exporter)(nil), NewInMemoryExporter()) -} - -func TestNewInMemoryExporter(t *testing.T) { - imsb := NewInMemoryExporter() - - require.NoError(t, imsb.Export(context.Background(), nil)) - assert.Len(t, imsb.GetRecords(), 0) - - input := make([]sdklog.Record, 10) - for i := 0; i < 10; i++ { - input[i] = sdklog.Record{} - input[i].SetBody(log.StringValue(fmt.Sprintf("record %d", i))) - } - require.NoError(t, imsb.Export(context.Background(), input)) - sds := imsb.GetRecords() - assert.Len(t, sds, 10) - for i, sd := range sds { - assert.Equal(t, input[i], sd) - } - imsb.Reset() - // Ensure that operations on the internal storage does not change the previously returned value. - assert.Len(t, sds, 10) - assert.Len(t, imsb.GetRecords(), 0) - - require.NoError(t, imsb.Export(context.Background(), input[0:1])) - sds = imsb.GetRecords() - assert.Len(t, sds, 1) - assert.Equal(t, input[0], sds[0]) -} - -func TestInMemoryExporterReset(t *testing.T) { - imsb := NewInMemoryExporter() - - input := make([]sdklog.Record, 10) - for i := 0; i < 10; i++ { - input[i] = sdklog.Record{} - input[i].SetBody(log.StringValue(fmt.Sprintf("record %d", i))) - } - require.NoError(t, imsb.Export(context.Background(), input)) - - assert.Len(t, imsb.GetRecords(), 10) - imsb.Reset() - assert.Len(t, imsb.GetRecords(), 0) -}