-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 <[email protected]> * kUpdate log/logtest/config.go Co-authored-by: Robert Pająk <[email protected]> * 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 <[email protected]> * create empty recorder in concurrent safe test * Update log/logtest/recorder_test.go Co-authored-by: Robert Pająk <[email protected]> * fix lint * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * Update log/logtest/recorder.go Co-authored-by: Tyler Yahn <[email protected]> * make enabledFunc callable from outside the package * replace expected with want --------- Co-authored-by: Robert Pająk <[email protected]> Co-authored-by: Tyler Yahn <[email protected]> Co-authored-by: Sam Xie <[email protected]>
- Loading branch information
1 parent
b9752eb
commit 8d1d62b
Showing
4 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Log Test | ||
|
||
[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/log/logtest)](https://pkg.go.dev/go.opentelemetry.io/otel/log/logtest) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |