You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
logtest: Add Recorder (#5134)
* introduce in-memory log exporter * add changelog entry * move logtest into a recorder within the api * rename GetRecords to Result * rename InMemoryRecorder to Recorder * name the struct r * ensure Logger creates a struct copy * replace severity with enabledFn * Update CHANGELOG.md Co-authored-by: Robert Pająk <pellared@hotmail.com> * kUpdate log/logtest/config.go Co-authored-by: Robert Pająk <pellared@hotmail.com> * store all scope records, so we can retrieve everything with `Result()` * store child loggers instead of all scope records * no need to explicitly create a new slice * add concurrent safe test * handle default enabled function if the struct was manually created * rename WithEnabledFn to WithEnabledFunc * test result/reset with child loggers * add enabled to concurrent safe * fix lint missing period * rename defaultEnabledFn to defaultEnabledFunc * merge recorder.go and config.go * Update log/logtest/recorder_test.go Co-authored-by: Robert Pająk <pellared@hotmail.com> * create empty recorder in concurrent safe test * Update log/logtest/recorder_test.go Co-authored-by: Robert Pająk <pellared@hotmail.com> * fix lint * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> * make enabledFunc callable from outside the package * replace expected with want --------- Co-authored-by: Robert Pająk <pellared@hotmail.com> Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Sam Xie <sam@samxie.me>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Log Test
|
||||
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel/log/logtest)
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package logtest is a testing helper package. Users 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 enabledFn func(context.Context, log.Record) bool
|
||||
|
||||
var defaultEnabledFunc = func(context.Context, log.Record) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type config struct {
|
||||
enabledFn enabledFn
|
||||
}
|
||||
|
||||
func newConfig(options []Option) config {
|
||||
var c config
|
||||
for _, opt := range options {
|
||||
c = opt.apply(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Option configures a [Recorder].
|
||||
type Option interface {
|
||||
apply(config) config
|
||||
}
|
||||
|
||||
type optFunc func(config) config
|
||||
|
||||
func (f optFunc) apply(c config) config { return f(c) }
|
||||
|
||||
// WithEnabledFunc allows configuring whether the [Recorder] is enabled for specific log entries or not.
|
||||
//
|
||||
// By default, the Recorder is enabled for every log entry.
|
||||
func WithEnabledFunc(fn func(context.Context, log.Record) bool) Option {
|
||||
return optFunc(func(c config) config {
|
||||
c.enabledFn = fn
|
||||
return c
|
||||
})
|
||||
}
|
||||
|
||||
// NewRecorder returns a new [Recorder].
|
||||
func NewRecorder(options ...Option) *Recorder {
|
||||
cfg := newConfig(options)
|
||||
|
||||
sr := &ScopeRecords{}
|
||||
|
||||
return &Recorder{
|
||||
currentScopeRecord: sr,
|
||||
enabledFn: cfg.enabledFn,
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeRecords represents the records for a single instrumentation scope.
|
||||
type ScopeRecords 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
|
||||
|
||||
// Records are the log records this instrumentation scope recorded.
|
||||
Records []log.Record
|
||||
}
|
||||
|
||||
// Recorder is a recorder that stores all received log records
|
||||
// in-memory.
|
||||
type Recorder struct {
|
||||
embedded.LoggerProvider
|
||||
embeddedLogger // nolint:unused // Used to embed embedded.Logger.
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
loggers []*Recorder
|
||||
currentScopeRecord *ScopeRecords
|
||||
|
||||
// enabledFn decides whether the recorder should enable logging of a record or not
|
||||
enabledFn enabledFn
|
||||
}
|
||||
|
||||
// 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...)
|
||||
|
||||
nr := &Recorder{
|
||||
currentScopeRecord: &ScopeRecords{
|
||||
Name: name,
|
||||
Version: cfg.InstrumentationVersion(),
|
||||
SchemaURL: cfg.SchemaURL(),
|
||||
},
|
||||
enabledFn: r.enabledFn,
|
||||
}
|
||||
r.addChildLogger(nr)
|
||||
|
||||
return nr
|
||||
}
|
||||
|
||||
func (r *Recorder) addChildLogger(nr *Recorder) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.loggers = append(r.loggers, nr)
|
||||
}
|
||||
|
||||
// Enabled indicates whether a specific record should be stored.
|
||||
func (r *Recorder) Enabled(ctx context.Context, record log.Record) bool {
|
||||
if r.enabledFn == nil {
|
||||
return defaultEnabledFunc(ctx, record)
|
||||
}
|
||||
|
||||
return r.enabledFn(ctx, record)
|
||||
}
|
||||
|
||||
// Emit stores the log record.
|
||||
func (r *Recorder) Emit(_ context.Context, record log.Record) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.currentScopeRecord.Records = append(r.currentScopeRecord.Records, record)
|
||||
}
|
||||
|
||||
// Result returns the current in-memory recorder log records.
|
||||
func (r *Recorder) Result() []*ScopeRecords {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
ret := []*ScopeRecords{}
|
||||
ret = append(ret, r.currentScopeRecord)
|
||||
for _, l := range r.loggers {
|
||||
ret = append(ret, l.Result()...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Reset clears the in-memory log records.
|
||||
func (r *Recorder) Reset() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.currentScopeRecord != nil {
|
||||
r.currentScopeRecord.Records = nil
|
||||
}
|
||||
for _, l := range r.loggers {
|
||||
l.Reset()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package logtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.opentelemetry.io/otel/log"
|
||||
)
|
||||
|
||||
func TestRecorderLogger(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
options []Option
|
||||
|
||||
loggerName string
|
||||
loggerOptions []log.LoggerOption
|
||||
|
||||
wantLogger log.Logger
|
||||
}{
|
||||
{
|
||||
name: "provides a default logger",
|
||||
|
||||
wantLogger: &Recorder{
|
||||
currentScopeRecord: &ScopeRecords{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provides a logger with a configured scope",
|
||||
|
||||
loggerName: "test",
|
||||
loggerOptions: []log.LoggerOption{
|
||||
log.WithInstrumentationVersion("logtest v42"),
|
||||
log.WithSchemaURL("https://example.com"),
|
||||
},
|
||||
|
||||
wantLogger: &Recorder{
|
||||
currentScopeRecord: &ScopeRecords{
|
||||
Name: "test",
|
||||
Version: "logtest v42",
|
||||
SchemaURL: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := NewRecorder(tt.options...).Logger(tt.loggerName, tt.loggerOptions...)
|
||||
// unset enabledFn to allow comparison
|
||||
l.(*Recorder).enabledFn = nil
|
||||
|
||||
assert.Equal(t, tt.wantLogger, l)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecorderLoggerCreatesNewStruct(t *testing.T) {
|
||||
r := &Recorder{}
|
||||
assert.NotEqual(t, r, r.Logger("test"))
|
||||
}
|
||||
|
||||
func TestRecorderEnabled(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 every log entry",
|
||||
ctx: context.Background(),
|
||||
buildRecord: func() log.Record {
|
||||
return log.Record{}
|
||||
},
|
||||
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "with everything disabled",
|
||||
options: []Option{
|
||||
WithEnabledFunc(func(context.Context, log.Record) bool {
|
||||
return false
|
||||
}),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
buildRecord: func() log.Record {
|
||||
return log.Record{}
|
||||
},
|
||||
|
||||
isEnabled: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := NewRecorder(tt.options...).Enabled(tt.ctx, tt.buildRecord())
|
||||
assert.Equal(t, tt.isEnabled, e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecorderEnabledFnUnset(t *testing.T) {
|
||||
r := &Recorder{}
|
||||
assert.True(t, r.Enabled(context.Background(), log.Record{}))
|
||||
}
|
||||
|
||||
func TestRecorderEmitAndReset(t *testing.T) {
|
||||
r := NewRecorder()
|
||||
assert.Len(t, r.Result()[0].Records, 0)
|
||||
|
||||
r1 := log.Record{}
|
||||
r1.SetSeverity(log.SeverityInfo)
|
||||
r.Emit(context.Background(), r1)
|
||||
assert.Equal(t, r.Result()[0].Records, []log.Record{r1})
|
||||
|
||||
l := r.Logger("test")
|
||||
assert.Empty(t, r.Result()[1].Records)
|
||||
|
||||
r2 := log.Record{}
|
||||
r2.SetSeverity(log.SeverityError)
|
||||
l.Emit(context.Background(), r2)
|
||||
assert.Equal(t, r.Result()[0].Records, []log.Record{r1})
|
||||
assert.Equal(t, r.Result()[1].Records, []log.Record{r2})
|
||||
|
||||
r.Reset()
|
||||
assert.Empty(t, r.Result()[0].Records)
|
||||
assert.Empty(t, r.Result()[1].Records)
|
||||
}
|
||||
|
||||
func TestRecorderConcurrentSafe(t *testing.T) {
|
||||
const goRoutineN = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goRoutineN)
|
||||
|
||||
r := &Recorder{}
|
||||
|
||||
for i := 0; i < goRoutineN; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
nr := r.Logger("test")
|
||||
nr.Enabled(context.Background(), log.Record{})
|
||||
nr.Emit(context.Background(), log.Record{})
|
||||
|
||||
r.Result()
|
||||
r.Reset()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user