Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logtest: Add Recorder #5134

Merged
merged 40 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dd4eeed
introduce in-memory log exporter
dmathieu Apr 2, 2024
675010f
add changelog entry
dmathieu Apr 2, 2024
6067a31
move logtest into a recorder within the api
dmathieu Apr 3, 2024
f573e8c
rename GetRecords to Result
dmathieu Apr 3, 2024
17258ab
rename InMemoryRecorder to Recorder
dmathieu Apr 3, 2024
ab7b0b9
name the struct r
dmathieu Apr 3, 2024
13c846f
ensure Logger creates a struct copy
dmathieu Apr 3, 2024
99a5be2
replace severity with enabledFn
dmathieu Apr 3, 2024
3f45b62
Update CHANGELOG.md
dmathieu Apr 3, 2024
b26d753
kUpdate log/logtest/config.go
dmathieu Apr 3, 2024
eeb18fe
store all scope records, so we can retrieve everything with `Result()`
dmathieu Apr 3, 2024
c5e4042
store child loggers instead of all scope records
dmathieu Apr 3, 2024
32f037d
no need to explicitly create a new slice
dmathieu Apr 3, 2024
e1feae1
add concurrent safe test
dmathieu Apr 3, 2024
108a6a1
handle default enabled function if the struct was manually created
dmathieu Apr 3, 2024
254b7cc
rename WithEnabledFn to WithEnabledFunc
dmathieu Apr 3, 2024
7bb8b22
test result/reset with child loggers
dmathieu Apr 3, 2024
5386696
add enabled to concurrent safe
dmathieu Apr 3, 2024
791f807
fix lint missing period
dmathieu Apr 3, 2024
1edddf7
rename defaultEnabledFn to defaultEnabledFunc
dmathieu Apr 3, 2024
1b7134c
merge recorder.go and config.go
dmathieu Apr 3, 2024
ae0607e
Update log/logtest/recorder_test.go
dmathieu Apr 3, 2024
99e51ee
create empty recorder in concurrent safe test
dmathieu Apr 3, 2024
1f2e9d3
Update log/logtest/recorder_test.go
dmathieu Apr 3, 2024
d8227d2
fix lint
dmathieu Apr 3, 2024
fedb9e0
Merge branch 'main' into logtest
pellared Apr 3, 2024
98a4daa
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
14840ec
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
780677b
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
603b567
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
4fd20be
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
dd01844
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
1231e46
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
6f87408
Update log/logtest/recorder.go
dmathieu Apr 8, 2024
0fb9304
Merge branch 'main' into logtest
dmathieu Apr 8, 2024
aa62736
make enabledFunc callable from outside the package
dmathieu Apr 8, 2024
ad46768
Merge branch 'main' into logtest
XSAM Apr 8, 2024
6e3ed39
replace expected with want
dmathieu Apr 9, 2024
edb2696
Merge branch 'main' into logtest
dmathieu Apr 9, 2024
a6d4db0
Merge branch 'main' into logtest
pellared Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `String` method to `Value` and `KeyValue` in `go.opentelemetry.io/otel/log`. (#5117)
- Add Exemplar support to `go.opentelemetry.io/otel/exporters/prometheus`. (#5111)
- Add metric semantic conventions to `go.opentelemetry.io/otel/semconv/v1.24.0`. Future `semconv` packages will include metric semantic conventions as well. (#4528)
- Add `Recorder` in `go.opentelemetry.io/otel/log/logtest` to facilitate testing the log bridge implementations. (#5134)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions log/logtest/README.md
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)
164 changes: 164 additions & 0 deletions log/logtest/recorder.go
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
dashpole marked this conversation as resolved.
Show resolved Hide resolved
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)
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
}

// 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()
}
}
156 changes: 156 additions & 0 deletions log/logtest/recorder_test.go
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) {
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
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()
}