Skip to content

Commit cfeb2f4

Browse files
committed
log/logtest: Change Recorder.Result
1 parent 95e5bba commit cfeb2f4

File tree

2 files changed

+158
-98
lines changed

2 files changed

+158
-98
lines changed

log/logtest/recorder.go

+101-51
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package logtest // import "go.opentelemetry.io/otel/log/logtest"
66
import (
77
"context"
88
"sync"
9+
"time"
910

1011
"go.opentelemetry.io/otel/attribute"
1112
"go.opentelemetry.io/otel/log"
@@ -59,126 +60,175 @@ func NewRecorder(options ...Option) *Recorder {
5960
}
6061
}
6162

62-
// ScopeRecords represents the records for a single instrumentation scope.
63-
type ScopeRecords struct {
64-
// Name is the name of the instrumentation scope.
63+
// Recording represents the recorded log records snapshot.
64+
type Recording map[Scope][]Record
65+
66+
// Scope represents the instrumentation scope.
67+
type Scope struct {
68+
// Name is the name of the instrumentation scope. This should be the
69+
// Go package name of that scope.
6570
Name string
6671
// Version is the version of the instrumentation scope.
6772
Version string
6873
// SchemaURL of the telemetry emitted by the scope.
6974
SchemaURL string
7075
// Attributes of the telemetry emitted by the scope.
7176
Attributes attribute.Set
72-
73-
// Records are the log records, and their associated context this
74-
// instrumentation scope recorded.
75-
Records []EmittedRecord
7677
}
7778

78-
// EmittedRecord holds a log record the instrumentation received, alongside its
79-
// context.
80-
type EmittedRecord struct {
81-
log.Record
82-
83-
ctx context.Context
84-
}
79+
// Record represents the record alongside its context.
80+
type Record struct {
81+
// Ensure forward compatibility by explicitly making this not comparable.
82+
_ [0]func()
8583

86-
// Context provides the context emitted with the record.
87-
func (rwc EmittedRecord) Context() context.Context {
88-
return rwc.ctx
84+
Context context.Context
85+
EventName string
86+
Timestamp time.Time
87+
ObservedTimestamp time.Time
88+
Severity log.Severity
89+
SeverityText string
90+
Body log.Value
91+
Attributes []log.KeyValue
8992
}
9093

91-
// Recorder is a recorder that stores all received log records
92-
// in-memory.
94+
// Recorder stores all received log records in-memory.
95+
// Recorder implements [log.LoggerProvider].
9396
type Recorder struct {
97+
// Ensure forward compatibility by explicitly making this not comparable.
98+
_ [0]func()
99+
94100
embedded.LoggerProvider
95101

96102
mu sync.Mutex
97-
loggers []*logger
103+
loggers map[Scope]*logger
98104

99105
// enabledFn decides whether the recorder should enable logging of a record or not
100106
enabledFn enabledFn
101107
}
102108

109+
// Compile-time check Recorder implements log.LoggerProvider.
110+
var _ log.LoggerProvider = (*Recorder)(nil)
111+
112+
// Clone returns a deep copy.
113+
func (a Record) Clone() Record {
114+
b := a
115+
attrs := make([]log.KeyValue, len(a.Attributes))
116+
copy(attrs, a.Attributes)
117+
b.Attributes = attrs
118+
return b
119+
}
120+
103121
// Logger returns a copy of Recorder as a [log.Logger] with the provided scope
104122
// information.
105123
func (r *Recorder) Logger(name string, opts ...log.LoggerOption) log.Logger {
106124
cfg := log.NewLoggerConfig(opts...)
107-
108-
nl := &logger{
109-
scopeRecord: &ScopeRecords{
110-
Name: name,
111-
Version: cfg.InstrumentationVersion(),
112-
SchemaURL: cfg.SchemaURL(),
113-
Attributes: cfg.InstrumentationAttributes(),
114-
},
115-
enabledFn: r.enabledFn,
125+
scope := Scope{
126+
Name: name,
127+
Version: cfg.InstrumentationVersion(),
128+
SchemaURL: cfg.SchemaURL(),
129+
Attributes: cfg.InstrumentationAttributes(),
116130
}
117-
r.addChildLogger(nl)
118-
119-
return nl
120-
}
121131

122-
func (r *Recorder) addChildLogger(nl *logger) {
123132
r.mu.Lock()
124133
defer r.mu.Unlock()
125134

126-
r.loggers = append(r.loggers, nl)
135+
if r.loggers == nil {
136+
r.loggers = make(map[Scope]*logger)
137+
}
138+
139+
l, ok := r.loggers[scope]
140+
if ok {
141+
return l
142+
}
143+
l = &logger{
144+
enabledFn: r.enabledFn,
145+
}
146+
r.loggers[scope] = l
147+
return l
127148
}
128149

129-
// Result returns the current in-memory recorder log records.
130-
func (r *Recorder) Result() []*ScopeRecords {
150+
// Reset clears the in-memory log records for all loggers.
151+
func (r *Recorder) Reset() {
131152
r.mu.Lock()
132153
defer r.mu.Unlock()
133154

134-
ret := []*ScopeRecords{}
135155
for _, l := range r.loggers {
136-
ret = append(ret, l.scopeRecord)
156+
l.Reset()
137157
}
138-
return ret
139158
}
140159

141-
// Reset clears the in-memory log records for all loggers.
142-
func (r *Recorder) Reset() {
160+
// Result returns a deep copy of the current in-memory recorded log records.
161+
func (r *Recorder) Result() Recording {
143162
r.mu.Lock()
144163
defer r.mu.Unlock()
145164

146-
for _, l := range r.loggers {
147-
l.Reset()
165+
res := make(Recording, len(r.loggers))
166+
for s, l := range r.loggers {
167+
func() {
168+
l.mu.Lock()
169+
defer l.mu.Unlock()
170+
if l.records == nil {
171+
res[s] = nil
172+
return
173+
}
174+
recs := make([]Record, len(l.records))
175+
for i, r := range l.records {
176+
recs[i] = r.Clone()
177+
}
178+
res[s] = recs
179+
}()
148180
}
181+
return res
149182
}
150183

151184
type logger struct {
152185
embedded.Logger
153186

154-
mu sync.Mutex
155-
scopeRecord *ScopeRecords
187+
mu sync.Mutex
188+
records []*Record
156189

157190
// enabledFn decides whether the recorder should enable logging of a record or not.
158191
enabledFn enabledFn
159192
}
160193

161194
// Enabled indicates whether a specific record should be stored.
162-
func (l *logger) Enabled(ctx context.Context, opts log.EnabledParameters) bool {
195+
func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
163196
if l.enabledFn == nil {
164-
return defaultEnabledFunc(ctx, opts)
197+
return defaultEnabledFunc(ctx, param)
165198
}
166199

167-
return l.enabledFn(ctx, opts)
200+
return l.enabledFn(ctx, param)
168201
}
169202

170203
// Emit stores the log record.
171204
func (l *logger) Emit(ctx context.Context, record log.Record) {
172205
l.mu.Lock()
173206
defer l.mu.Unlock()
174207

175-
l.scopeRecord.Records = append(l.scopeRecord.Records, EmittedRecord{record, ctx})
208+
attrs := make([]log.KeyValue, 0, record.AttributesLen())
209+
record.WalkAttributes(func(kv log.KeyValue) bool {
210+
attrs = append(attrs, kv)
211+
return true
212+
})
213+
214+
r := &Record{
215+
Context: ctx,
216+
EventName: record.EventName(),
217+
Timestamp: record.Timestamp(),
218+
ObservedTimestamp: record.ObservedTimestamp(),
219+
Severity: record.Severity(),
220+
SeverityText: record.SeverityText(),
221+
Body: record.Body(),
222+
Attributes: attrs,
223+
}
224+
225+
l.records = append(l.records, r)
176226
}
177227

178228
// Reset clears the in-memory log records.
179229
func (l *logger) Reset() {
180230
l.mu.Lock()
181231
defer l.mu.Unlock()
182232

183-
l.scopeRecord.Records = nil
233+
l.records = nil
184234
}

log/logtest/recorder_test.go

+57-47
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"sync"
99
"testing"
10+
"time"
1011

1112
"github.com/stretchr/testify/assert"
1213

@@ -22,41 +23,37 @@ func TestRecorderLogger(t *testing.T) {
2223
loggerName string
2324
loggerOptions []log.LoggerOption
2425

25-
wantLogger log.Logger
26+
want Recording
2627
}{
2728
{
28-
name: "provides a default logger",
29-
30-
wantLogger: &logger{
31-
scopeRecord: &ScopeRecords{},
29+
name: "default scope",
30+
want: Recording{
31+
Scope{}: nil,
3232
},
3333
},
3434
{
35-
name: "provides a logger with a configured scope",
36-
35+
name: "configured scope",
3736
loggerName: "test",
3837
loggerOptions: []log.LoggerOption{
3938
log.WithInstrumentationVersion("logtest v42"),
4039
log.WithSchemaURL("https://example.com"),
4140
log.WithInstrumentationAttributes(attribute.String("foo", "bar")),
4241
},
43-
44-
wantLogger: &logger{
45-
scopeRecord: &ScopeRecords{
42+
want: Recording{
43+
Scope{
4644
Name: "test",
4745
Version: "logtest v42",
4846
SchemaURL: "https://example.com",
4947
Attributes: attribute.NewSet(attribute.String("foo", "bar")),
50-
},
48+
}: nil,
5149
},
5250
},
5351
} {
5452
t.Run(tt.name, func(t *testing.T) {
55-
l := NewRecorder(tt.options...).Logger(tt.loggerName, tt.loggerOptions...)
56-
// unset enabledFn to allow comparison
57-
l.(*logger).enabledFn = nil
58-
59-
assert.Equal(t, tt.wantLogger, l)
53+
rec := NewRecorder(tt.options...)
54+
rec.Logger(tt.loggerName, tt.loggerOptions...)
55+
got := rec.Result()
56+
assert.Equal(t, tt.want, got)
6057
})
6158
}
6259
}
@@ -102,41 +99,54 @@ func TestLoggerEnabledFnUnset(t *testing.T) {
10299
assert.True(t, r.Enabled(context.Background(), log.EnabledParameters{}))
103100
}
104101

105-
func TestRecorderEmitAndReset(t *testing.T) {
106-
r := NewRecorder()
107-
l := r.Logger("test")
108-
assert.Empty(t, r.Result()[0].Records)
102+
func TestRecorderLoggerEmitAndReset(t *testing.T) {
103+
rec := NewRecorder()
104+
ts := time.Now()
109105

110-
r1 := log.Record{}
111-
r1.SetSeverity(log.SeverityInfo)
106+
l := rec.Logger(t.Name())
112107
ctx := context.Background()
108+
r := log.Record{}
109+
r.SetTimestamp(ts)
110+
r.SetSeverity(log.SeverityInfo)
111+
r.SetBody(log.StringValue("Hello there"))
112+
r.AddAttributes(log.Int("n", 1))
113+
r.AddAttributes(log.String("foo", "bar"))
114+
l.Emit(ctx, r)
115+
116+
l2 := rec.Logger(t.Name())
117+
r2 := log.Record{}
118+
r2.SetBody(log.StringValue("Logger with the same scope"))
119+
l2.Emit(ctx, r2)
120+
121+
want := Recording{
122+
Scope{Name: t.Name()}: []Record{
123+
{
124+
Context: ctx,
125+
Timestamp: ts,
126+
Severity: log.SeverityInfo,
127+
Body: log.StringValue("Hello there"),
128+
Attributes: []log.KeyValue{
129+
log.Int("n", 1),
130+
log.String("foo", "bar"),
131+
},
132+
},
133+
{
134+
Context: ctx,
135+
Body: log.StringValue("Logger with the same scope"),
136+
Attributes: []log.KeyValue{},
137+
},
138+
},
139+
}
140+
got := rec.Result()
141+
assert.Equal(t, want, got)
113142

114-
l.Emit(ctx, r1)
115-
assert.Equal(t, []EmittedRecord{
116-
{r1, ctx},
117-
}, r.Result()[0].Records)
118-
119-
nl := r.Logger("test")
120-
assert.Empty(t, r.Result()[1].Records)
143+
rec.Reset()
121144

122-
r2 := log.Record{}
123-
r2.SetSeverity(log.SeverityError)
124-
// We want a non-background context here so it's different from `ctx`.
125-
ctx2, cancel := context.WithCancel(ctx)
126-
defer cancel()
127-
128-
nl.Emit(ctx2, r2)
129-
assert.Len(t, r.Result()[0].Records, 1)
130-
AssertRecordEqual(t, r.Result()[0].Records[0].Record, r1)
131-
assert.Equal(t, r.Result()[0].Records[0].Context(), ctx)
132-
133-
assert.Len(t, r.Result()[1].Records, 1)
134-
AssertRecordEqual(t, r.Result()[1].Records[0].Record, r2)
135-
assert.Equal(t, r.Result()[1].Records[0].Context(), ctx2)
136-
137-
r.Reset()
138-
assert.Empty(t, r.Result()[0].Records)
139-
assert.Empty(t, r.Result()[1].Records)
145+
want = Recording{
146+
Scope{Name: t.Name()}: nil,
147+
}
148+
got = rec.Result()
149+
assert.Equal(t, want, got)
140150
}
141151

142152
func TestRecorderConcurrentSafe(t *testing.T) {

0 commit comments

Comments
 (0)