diff --git a/CHANGELOG.md b/CHANGELOG.md index edc38211738..1b7d637ceb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] -### Removed +### Added -- Drop support for [Go 1.22]. (#6381, #6418) +- Add `Recording`, `Scope`, `Record` types in `go.opentelemetry.io/otel/log/logtest`. (#6342) +- Add `AssertEqual` function along with `IgnoreTimestamp` option in `go.opentelemetry.io/otel/log/logtest`. (#6342) +- Add a testable example showing how `go.opentelemetry.io/otel/log/logtest` can be used. (#6342) ### Changed @@ -18,12 +20,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm This allows metrics names to keep original delimiters (e.g. `.`), rather than replacing with underscores. This can be reverted by setting `github.com/prometheus/common/model.NameValidationScheme` to `LegacyValidation` in `github.com/prometheus/common/model`. (#6433) - Initialize map with `len(keys)` in `NewAllowKeysFilter` and `NewDenyKeysFilter` to avoid unnecessary allocations in `go.opentelemetry.io/otel/attribute`. (#6455) +- Change `Recorder.Result` to return `Recording`. (#6342) +- `Recorder` no longer separately stores records emitted by loggers with the same instrumentation scope. (#6342) ### Fixes - Stop percent encoding header environment variables in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc` and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#6392) - Ensure the `noopSpan.tracerProvider` method is not inlined in `go.opentelemetry.io/otel/trace` so the `go.opentelemetry.io/auto` instrumentation can instrument non-recording spans. (#6456) +### Removed + +- Drop support for [Go 1.22]. (#6381, #6418) +- Remove `ScopeRecords`, `EmittedRecord`, `RecordFactory` types from `go.opentelemetry.io/otel/log/logtest`. (#6342) +- Remove `AssertRecordEqual` function from `go.opentelemetry.io/otel/log/logtest`. (#6342) + diff --git a/log/go.mod b/log/go.mod index e97b5a3110f..87aec04bc06 100644 --- a/log/go.mod +++ b/log/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/go-logr/logr v1.4.2 + github.com/google/go-cmp v0.7.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 ) diff --git a/log/logtest/assert.go b/log/logtest/assert.go new file mode 100644 index 00000000000..28321437b4b --- /dev/null +++ b/log/logtest/assert.go @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest // import "go.opentelemetry.io/otel/log/logtest" + +import ( + "context" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "go.opentelemetry.io/otel/log" +) + +// TestingT reports failure messages. +// [testing.T] implements this interface. +type TestingT interface { + Errorf(format string, args ...any) +} + +// AssertEqual asserts that the two concrete data-types from the logtest package are equal. +func AssertEqual[T Recording | Record](t TestingT, want, got T, opts ...AssertOption) bool { + if h, ok := t.(interface{ Helper() }); ok { + h.Helper() + } + + cmpOpts := []cmp.Option{ + cmp.Comparer(func(x, y context.Context) bool { return x == y }), // Compare context. + cmpopts.SortSlices(func(a, b log.KeyValue) bool { return a.Key < b.Key }), // Unordered compare of the key values. + cmpopts.EquateEmpty(), // Empty and nil collections are equal. + } + + cfg := newAssertConfig(opts) + if cfg.ignoreTimestamp { + cmpOpts = append(cmpOpts, cmpopts.IgnoreTypes(time.Time{})) // Ignore Timestamps. + } + + if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + return false + } + return true +} + +type assertConfig struct { + ignoreTimestamp bool +} + +func newAssertConfig(opts []AssertOption) assertConfig { + var cfg assertConfig + for _, opt := range opts { + cfg = opt.apply(cfg) + } + return cfg +} + +// AssertOption allows for fine grain control over how AssertEqual operates. +type AssertOption interface { + apply(cfg assertConfig) assertConfig +} + +type fnOption func(cfg assertConfig) assertConfig + +func (fn fnOption) apply(cfg assertConfig) assertConfig { + return fn(cfg) +} + +// IgnoreTimestamp disables checking if timestamps are different. +func IgnoreTimestamp() AssertOption { + return fnOption(func(cfg assertConfig) assertConfig { + cfg.ignoreTimestamp = true + return cfg + }) +} diff --git a/log/logtest/assert_test.go b/log/logtest/assert_test.go new file mode 100644 index 00000000000..f280a658ae6 --- /dev/null +++ b/log/logtest/assert_test.go @@ -0,0 +1,174 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest + +import ( + "context" + "testing" + "time" + + "go.opentelemetry.io/otel/log" +) + +var y2k = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + +type mockTestingT struct { + errors []string +} + +func (m *mockTestingT) Errorf(format string, args ...any) { + m.errors = append(m.errors, format) +} + +func TestAssertEqualRecording(t *testing.T) { + tests := []struct { + name string + a Recording + b Recording + opts []AssertOption + want bool + }{ + { + name: "equal recordings", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Timestamp: y2k, Context: context.Background(), Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Timestamp: y2k, Context: context.Background(), Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}}, + }, + }, + want: true, + }, + { + name: "different recordings", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Attributes: []log.KeyValue{log.String("foo", "bar")}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Attributes: []log.KeyValue{log.Int("n", 1)}}, + }, + }, + want: false, + }, + { + name: "equal empty scopes", + a: Recording{ + Scope{Name: t.Name()}: nil, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{}, + }, + want: true, + }, + { + name: "equal empty attributes", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: []log.KeyValue{}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Body: log.StringValue("msg"), Attributes: nil}, + }, + }, + want: true, + }, + { + name: "ignore timestamp", + a: Recording{ + Scope{Name: t.Name()}: []Record{ + {Timestamp: y2k, Attributes: []log.KeyValue{log.String("foo", "bar")}}, + }, + }, + b: Recording{ + Scope{Name: t.Name()}: []Record{ + {Timestamp: time.Now(), Attributes: []log.KeyValue{log.String("foo", "bar")}}, + }, + }, + opts: []AssertOption{IgnoreTimestamp()}, + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockT := &mockTestingT{} + result := AssertEqual(mockT, tc.a, tc.b, tc.opts...) + if result != tc.want { + t.Errorf("AssertEqual() = %v, want %v", result, tc.want) + } + if !tc.want && len(mockT.errors) == 0 { + t.Errorf("expected Errorf call but got none") + } + }) + } +} + +func TestAssertEqualRecord(t *testing.T) { + tests := []struct { + name string + a Record + b Record + opts []AssertOption + want bool + }{ + { + name: "equal records", + a: Record{ + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.Int("n", 1), log.String("foo", "bar")}, + }, + b: Record{ + Timestamp: y2k, + Context: context.Background(), + Attributes: []log.KeyValue{log.String("foo", "bar"), log.Int("n", 1)}, + }, + want: true, + }, + { + name: "different records", + a: Record{ + Attributes: []log.KeyValue{log.String("foo", "bar")}, + }, + b: Record{ + Attributes: []log.KeyValue{log.Int("n", 1)}, + }, + want: false, + }, + { + name: "ignore timestamp", + a: Record{ + Timestamp: y2k, + Attributes: []log.KeyValue{log.String("foo", "bar")}, + }, + b: Record{ + Timestamp: time.Now(), + Attributes: []log.KeyValue{log.String("foo", "bar")}, + }, + opts: []AssertOption{IgnoreTimestamp()}, + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockT := &mockTestingT{} + result := AssertEqual(mockT, tc.a, tc.b, tc.opts...) + if result != tc.want { + t.Errorf("AssertEqual() = %v, want %v", result, tc.want) + } + if !tc.want && len(mockT.errors) == 0 { + t.Errorf("expected Errorf call but got none") + } + }) + } +} diff --git a/log/logtest/assertions.go b/log/logtest/assertions.go deleted file mode 100644 index 479cc3b0876..00000000000 --- a/log/logtest/assertions.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest // import "go.opentelemetry.io/otel/log/logtest" - -import ( - "slices" - "testing" - - "go.opentelemetry.io/otel/log" -) - -// AssertRecordEqual compares two log records, and fails the test if they are -// not equal. -func AssertRecordEqual(t testing.TB, want, got log.Record) bool { - t.Helper() - - if want.EventName() != got.EventName() { - t.Errorf("EventName value is not equal:\nwant: %v\ngot: %v", want.EventName(), got.EventName()) - return false - } - if !want.Timestamp().Equal(got.Timestamp()) { - t.Errorf("Timestamp value is not equal:\nwant: %v\ngot: %v", want.Timestamp(), got.Timestamp()) - return false - } - if !want.ObservedTimestamp().Equal(got.ObservedTimestamp()) { - t.Errorf("ObservedTimestamp value is not equal:\nwant: %v\ngot: %v", want.ObservedTimestamp(), got.ObservedTimestamp()) - return false - } - if want.Severity() != got.Severity() { - t.Errorf("Severity value is not equal:\nwant: %v\ngot: %v", want.Severity(), got.Severity()) - return false - } - if want.SeverityText() != got.SeverityText() { - t.Errorf("SeverityText value is not equal:\nwant: %v\ngot: %v", want.SeverityText(), got.SeverityText()) - return false - } - if !assertBody(t, want.Body(), got) { - return false - } - - var attrs []log.KeyValue - want.WalkAttributes(func(kv log.KeyValue) bool { - attrs = append(attrs, kv) - return true - }) - return assertAttributes(t, attrs, got) -} - -func assertBody(t testing.TB, want log.Value, r log.Record) bool { - t.Helper() - got := r.Body() - if !got.Equal(want) { - t.Errorf("Body value is not equal:\nwant: %v\ngot: %v", want, got) - return false - } - - return true -} - -func assertAttributes(t testing.TB, want []log.KeyValue, r log.Record) bool { - t.Helper() - var got []log.KeyValue - r.WalkAttributes(func(kv log.KeyValue) bool { - got = append(got, kv) - return true - }) - if !slices.EqualFunc(want, got, log.KeyValue.Equal) { - t.Errorf("Attributes are not equal:\nwant: %v\ngot: %v", want, got) - return false - } - - return true -} diff --git a/log/logtest/assertions_test.go b/log/logtest/assertions_test.go deleted file mode 100644 index 04d62d15d35..00000000000 --- a/log/logtest/assertions_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest - -import ( - "testing" - "time" - - "go.opentelemetry.io/otel/log" -) - -func TestAssertRecord(t *testing.T) { - r1 := log.Record{} - r2 := log.Record{} - AssertRecordEqual(t, r1, r2) - - now := time.Now() - r1.SetEventName("my_event") - r2.SetEventName("my_event") - r1.SetTimestamp(now) - r2.SetTimestamp(now) - r1.SetObservedTimestamp(now) - r2.SetObservedTimestamp(now) - r1.SetSeverity(log.SeverityTrace1) - r2.SetSeverity(log.SeverityTrace1) - r1.SetSeverityText("trace") - r2.SetSeverityText("trace") - r1.SetBody(log.StringValue("log body")) - r2.SetBody(log.StringValue("log body")) - r1.AddAttributes(log.Bool("attr", true)) - r2.AddAttributes(log.Bool("attr", true)) - AssertRecordEqual(t, r1, r2) -} diff --git a/log/logtest/example_test.go b/log/logtest/example_test.go new file mode 100644 index 00000000000..1fc7b4dbb5d --- /dev/null +++ b/log/logtest/example_test.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logtest_test + +import ( + "context" + "testing" + "time" + + "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/logtest" +) + +func Example() { + t := &testing.T{} // Provided by testing framework. + + // Create a recorder. + rec := logtest.NewRecorder() + + // Emit a log record (code under test). + l := rec.Logger("Example") + ctx := context.Background() + r := log.Record{} + r.SetTimestamp(time.Now()) + r.SetSeverity(log.SeverityInfo) + r.SetBody(log.StringValue("Hello there")) + r.AddAttributes(log.String("foo", "bar")) + r.AddAttributes(log.Int("n", 1)) + l.Emit(ctx, r) + + // Verify that the expected and actual log records match. + want := logtest.Recording{ + logtest.Scope{Name: "Example"}: []logtest.Record{ + { + Context: context.Background(), + Severity: log.SeverityInfo, + Body: log.StringValue("Hello there"), + Attributes: []log.KeyValue{ + log.Int("n", 1), + log.String("foo", "bar"), + }, + }, + }, + } + got := rec.Result() + logtest.AssertEqual(t, want, got, logtest.IgnoreTimestamp()) + + // Output: + // +} diff --git a/log/logtest/factory.go b/log/logtest/factory.go deleted file mode 100644 index 0c3b389c3dc..00000000000 --- a/log/logtest/factory.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest // import "go.opentelemetry.io/otel/log/logtest" - -import ( - "time" - - "go.opentelemetry.io/otel/log" -) - -// RecordFactory is used to facilitate unit testing bridge implementations that -// make use of a [go.opentelemetry.io/otel/log.Record] -// -// Do not use RecordFactory to create records in production code. -type RecordFactory struct { - EventName string - Timestamp time.Time - ObservedTimestamp time.Time - Severity log.Severity - SeverityText string - Body log.Value - Attributes []log.KeyValue -} - -// NewRecord returns a log record. -func (b RecordFactory) NewRecord() log.Record { - var record log.Record - record.SetEventName(b.EventName) - record.SetTimestamp(b.Timestamp) - record.SetObservedTimestamp(b.ObservedTimestamp) - record.SetSeverity(b.Severity) - record.SetSeverityText(b.SeverityText) - record.SetBody(b.Body) - record.AddAttributes(b.Attributes...) - - return record -} diff --git a/log/logtest/factory_test.go b/log/logtest/factory_test.go deleted file mode 100644 index 084c31bc22f..00000000000 --- a/log/logtest/factory_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package logtest - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "go.opentelemetry.io/otel/log" -) - -func TestRecordFactory(t *testing.T) { - now := time.Now() - observed := now.Add(time.Second) - evnt := "my_event" - severity := log.SeverityDebug - severityText := "DBG" - body := log.StringValue("Message") - attrs := []log.KeyValue{ - log.Int("int", 1), - log.String("str", "foo"), - log.Float64("flt", 3.14), - } - - got := RecordFactory{ - EventName: evnt, - Timestamp: now, - ObservedTimestamp: observed, - Severity: severity, - SeverityText: severityText, - Body: body, - Attributes: attrs, - }.NewRecord() - - assert.Equal(t, evnt, got.EventName()) - assert.Equal(t, now, got.Timestamp()) - assert.Equal(t, observed, got.ObservedTimestamp()) - assert.Equal(t, severity, got.Severity()) - assert.Equal(t, severityText, got.SeverityText()) - assertBody(t, body, got) - assertAttributes(t, attrs, got) -} - -func TestRecordFactoryMultiple(t *testing.T) { - now := time.Now() - attrs := []log.KeyValue{ - log.Int("int", 1), - log.String("str", "foo"), - log.Float64("flt", 3.14), - } - - f := RecordFactory{ - Timestamp: now, - Attributes: attrs, - } - - record1 := f.NewRecord() - f.Attributes = append(f.Attributes, log.Bool("added", true)) - - record2 := f.NewRecord() - assert.Equal(t, now, record2.Timestamp()) - assertAttributes(t, append(attrs, log.Bool("added", true)), record2) - - // Previously returned record is unharmed by the builder changes. - assert.Equal(t, now, record1.Timestamp()) - assertAttributes(t, attrs, record1) -} diff --git a/log/logtest/recorder.go b/log/logtest/recorder.go index fd986c9afc4..dba810a2ff2 100644 --- a/log/logtest/recorder.go +++ b/log/logtest/recorder.go @@ -6,12 +6,31 @@ package logtest // import "go.opentelemetry.io/otel/log/logtest" import ( "context" "sync" + "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" ) +// Recorder stores all received log records in-memory. +// Recorder implements [log.LoggerProvider]. +type Recorder struct { + // Ensure forward compatibility by explicitly making this not comparable. + _ [0]func() + + embedded.LoggerProvider + + mu sync.Mutex + loggers map[Scope]*logger + + // enabledFn decides whether the recorder should enable logging of a record or not + enabledFn enabledFn +} + +// Compile-time check Recorder implements log.LoggerProvider. +var _ log.LoggerProvider = (*Recorder)(nil) + type enabledFn func(context.Context, log.EnabledParameters) bool var defaultEnabledFunc = func(context.Context, log.EnabledParameters) bool { @@ -55,13 +74,18 @@ func NewRecorder(options ...Option) *Recorder { cfg := newConfig(options) return &Recorder{ + loggers: make(map[Scope]*logger), enabledFn: cfg.enabledFn, } } -// ScopeRecords represents the records for a single instrumentation scope. -type ScopeRecords struct { - // Name is the name of the instrumentation scope. +// Recording represents the recorded log records snapshot. +type Recording map[Scope][]Record + +// Scope represents the instrumentation scope. +type Scope struct { + // Name is the name of the instrumentation scope. This should be the + // Go package name of that scope. Name string // Version is the version of the instrumentation scope. Version string @@ -69,73 +93,83 @@ type ScopeRecords struct { SchemaURL string // Attributes of the telemetry emitted by the scope. Attributes attribute.Set - - // Records are the log records, and their associated context this - // instrumentation scope recorded. - Records []EmittedRecord } -// EmittedRecord holds a log record the instrumentation received, alongside its -// context. -type EmittedRecord struct { - log.Record +// Record represents the record alongside its context. +type Record struct { + // Ensure forward compatibility by explicitly making this not comparable. + _ [0]func() - ctx context.Context + Context context.Context + EventName string + Timestamp time.Time + ObservedTimestamp time.Time + Severity log.Severity + SeverityText string + Body log.Value + Attributes []log.KeyValue } -// Context provides the context emitted with the record. -func (rwc EmittedRecord) Context() context.Context { - return rwc.ctx -} - -// Recorder is a recorder that stores all received log records -// in-memory. -type Recorder struct { - embedded.LoggerProvider - - mu sync.Mutex - loggers []*logger - - // enabledFn decides whether the recorder should enable logging of a record or not - enabledFn enabledFn +// Clone returns a deep copy. +func (a Record) Clone() Record { + b := a + attrs := make([]log.KeyValue, len(a.Attributes)) + copy(attrs, a.Attributes) + b.Attributes = attrs + return b } // Logger returns a copy of Recorder as a [log.Logger] with the provided scope // information. func (r *Recorder) Logger(name string, opts ...log.LoggerOption) log.Logger { cfg := log.NewLoggerConfig(opts...) - - nl := &logger{ - scopeRecord: &ScopeRecords{ - Name: name, - Version: cfg.InstrumentationVersion(), - SchemaURL: cfg.SchemaURL(), - Attributes: cfg.InstrumentationAttributes(), - }, - enabledFn: r.enabledFn, + scope := Scope{ + Name: name, + Version: cfg.InstrumentationVersion(), + SchemaURL: cfg.SchemaURL(), + Attributes: cfg.InstrumentationAttributes(), } - r.addChildLogger(nl) - return nl -} - -func (r *Recorder) addChildLogger(nl *logger) { r.mu.Lock() defer r.mu.Unlock() - r.loggers = append(r.loggers, nl) + if r.loggers == nil { + r.loggers = make(map[Scope]*logger) + } + + l, ok := r.loggers[scope] + if ok { + return l + } + l = &logger{ + enabledFn: r.enabledFn, + } + r.loggers[scope] = l + return l } -// Result returns the current in-memory recorder log records. -func (r *Recorder) Result() []*ScopeRecords { +// Result returns a deep copy of the current in-memory recorded log records. +func (r *Recorder) Result() Recording { r.mu.Lock() defer r.mu.Unlock() - ret := []*ScopeRecords{} - for _, l := range r.loggers { - ret = append(ret, l.scopeRecord) + res := make(Recording, len(r.loggers)) + for s, l := range r.loggers { + func() { + l.mu.Lock() + defer l.mu.Unlock() + if l.records == nil { + res[s] = nil + return + } + recs := make([]Record, len(l.records)) + for i, r := range l.records { + recs[i] = r.Clone() + } + res[s] = recs + }() } - return ret + return res } // Reset clears the in-memory log records for all loggers. @@ -151,20 +185,20 @@ func (r *Recorder) Reset() { type logger struct { embedded.Logger - mu sync.Mutex - scopeRecord *ScopeRecords + mu sync.Mutex + records []*Record // enabledFn decides whether the recorder should enable logging of a record or not. enabledFn enabledFn } // Enabled indicates whether a specific record should be stored. -func (l *logger) Enabled(ctx context.Context, opts log.EnabledParameters) bool { +func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool { if l.enabledFn == nil { - return defaultEnabledFunc(ctx, opts) + return defaultEnabledFunc(ctx, param) } - return l.enabledFn(ctx, opts) + return l.enabledFn(ctx, param) } // Emit stores the log record. @@ -172,7 +206,24 @@ func (l *logger) Emit(ctx context.Context, record log.Record) { l.mu.Lock() defer l.mu.Unlock() - l.scopeRecord.Records = append(l.scopeRecord.Records, EmittedRecord{record, ctx}) + attrs := make([]log.KeyValue, 0, record.AttributesLen()) + record.WalkAttributes(func(kv log.KeyValue) bool { + attrs = append(attrs, kv) + return true + }) + + r := &Record{ + Context: ctx, + EventName: record.EventName(), + Timestamp: record.Timestamp(), + ObservedTimestamp: record.ObservedTimestamp(), + Severity: record.Severity(), + SeverityText: record.SeverityText(), + Body: record.Body(), + Attributes: attrs, + } + + l.records = append(l.records, r) } // Reset clears the in-memory log records. @@ -180,5 +231,5 @@ func (l *logger) Reset() { l.mu.Lock() defer l.mu.Unlock() - l.scopeRecord.Records = nil + l.records = nil } diff --git a/log/logtest/recorder_test.go b/log/logtest/recorder_test.go index 28d814528e2..5e247d9a085 100644 --- a/log/logtest/recorder_test.go +++ b/log/logtest/recorder_test.go @@ -7,66 +7,52 @@ import ( "context" "sync" "testing" + "time" - "github.com/stretchr/testify/assert" - - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) -func TestRecorderLogger(t *testing.T) { - for _, tt := range []struct { - name string - options []Option - - loggerName string - loggerOptions []log.LoggerOption +func TestRecorderLoggerEmitAndReset(t *testing.T) { + rec := NewRecorder() + ts := time.Now() - wantLogger log.Logger - }{ - { - name: "provides a default logger", - - wantLogger: &logger{ - scopeRecord: &ScopeRecords{}, - }, - }, - { - name: "provides a logger with a configured scope", - - loggerName: "test", - loggerOptions: []log.LoggerOption{ - log.WithInstrumentationVersion("logtest v42"), - log.WithSchemaURL("https://example.com"), - log.WithInstrumentationAttributes(attribute.String("foo", "bar")), - }, - - wantLogger: &logger{ - scopeRecord: &ScopeRecords{ - Name: "test", - Version: "logtest v42", - SchemaURL: "https://example.com", - Attributes: attribute.NewSet(attribute.String("foo", "bar")), + l := rec.Logger(t.Name()) + ctx := context.Background() + r := log.Record{} + r.SetTimestamp(ts) + r.SetSeverity(log.SeverityInfo) + r.SetBody(log.StringValue("Hello there")) + r.AddAttributes(log.Int("n", 1)) + r.AddAttributes(log.String("foo", "bar")) + l.Emit(ctx, r) + + want := Recording{ + Scope{Name: t.Name()}: []Record{ + { + Context: ctx, + Timestamp: ts, + Severity: log.SeverityInfo, + Body: log.StringValue("Hello there"), + Attributes: []log.KeyValue{ + log.Int("n", 1), + log.String("foo", "bar"), }, }, }, - } { - t.Run(tt.name, func(t *testing.T) { - l := NewRecorder(tt.options...).Logger(tt.loggerName, tt.loggerOptions...) - // unset enabledFn to allow comparison - l.(*logger).enabledFn = nil - - assert.Equal(t, tt.wantLogger, l) - }) } -} + got := rec.Result() + AssertEqual(t, want, got) -func TestRecorderLoggerCreatesNewStruct(t *testing.T) { - r := &Recorder{} - assert.NotEqual(t, r, r.Logger("test")) + rec.Reset() + + want = Recording{ + Scope{Name: t.Name()}: nil, + } + got = rec.Result() + AssertEqual(t, want, got) } -func TestLoggerEnabled(t *testing.T) { +func TestRecorderLoggerEnabled(t *testing.T) { for _, tt := range []struct { name string options []Option @@ -91,54 +77,14 @@ func TestLoggerEnabled(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - e := NewRecorder(tt.options...).Logger("test").Enabled(tt.ctx, tt.enabledParams) - assert.Equal(t, tt.want, e) + got := NewRecorder(tt.options...).Logger("test").Enabled(tt.ctx, tt.enabledParams) + if got != tt.want { + t.Errorf("got: %v, want: %v", got, tt.want) + } }) } } -func TestLoggerEnabledFnUnset(t *testing.T) { - r := &logger{} - assert.True(t, r.Enabled(context.Background(), log.EnabledParameters{})) -} - -func TestRecorderEmitAndReset(t *testing.T) { - r := NewRecorder() - l := r.Logger("test") - assert.Empty(t, r.Result()[0].Records) - - r1 := log.Record{} - r1.SetSeverity(log.SeverityInfo) - ctx := context.Background() - - l.Emit(ctx, r1) - assert.Equal(t, []EmittedRecord{ - {r1, ctx}, - }, r.Result()[0].Records) - - nl := r.Logger("test") - assert.Empty(t, r.Result()[1].Records) - - r2 := log.Record{} - r2.SetSeverity(log.SeverityError) - // We want a non-background context here so it's different from `ctx`. - ctx2, cancel := context.WithCancel(ctx) - defer cancel() - - nl.Emit(ctx2, r2) - assert.Len(t, r.Result()[0].Records, 1) - AssertRecordEqual(t, r.Result()[0].Records[0].Record, r1) - assert.Equal(t, r.Result()[0].Records[0].Context(), ctx) - - assert.Len(t, r.Result()[1].Records, 1) - AssertRecordEqual(t, r.Result()[1].Records[0].Record, r2) - assert.Equal(t, r.Result()[1].Records[0].Context(), ctx2) - - r.Reset() - assert.Empty(t, r.Result()[0].Records) - assert.Empty(t, r.Result()[1].Records) -} - func TestRecorderConcurrentSafe(t *testing.T) { const goRoutineN = 10