Skip to content

log/logtest: Change Recorder.Result #6507

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

Merged
merged 12 commits into from
Apr 12, 2025
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `go.opentelemetry.io/otel/semconv/v1.31.0` package.
The package contains semantic conventions from the `v1.31.0` version of the OpenTelemetry Semantic Conventions.
See the [migration documentation](./semconv/v1.31.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.30.0`(#6479)
- Add `Recording`, `Scope`, and `Record` types in `go.opentelemetry.io/otel/log/logtest`. (#6507)

### Removed

- Drop support for [Go 1.22]. (#6381, #6418)
- Remove `Resource` field from `EnabledParameters` in `go.opentelemetry.io/otel/sdk/log`. (#6494)
- Remove `RecordFactory` type from `go.opentelemetry.io/otel/log/logtest`. (#6492)
- Remove `ScopeRecords`, `EmittedRecord`, and `RecordFactory` types from `go.opentelemetry.io/otel/log/logtest`. (#6507)

### Changed

Expand All @@ -29,6 +31,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Initialize map with `len(keys)` in `NewAllowKeysFilter` and `NewDenyKeysFilter` to avoid unnecessary allocations in `go.opentelemetry.io/otel/attribute`. (#6455)
- `go.opentelemetry.io/otel/log/logtest` is now a separate Go module. (#6465)
- `go.opentelemetry.io/otel/sdk/log/logtest` is now a separate Go module. (#6466)
- `Recorder` in `go.opentelemetry.io/otel/log/logtest` no longer separately stores records emitted by loggers with the same instrumentation scope. (#6507)
- Improve performance of `BatchProcessor` in `go.opentelemetry.io/otel/sdk/log` by not exporting when exporter cannot accept more. (#6569, #6641)

### Deprecated
Expand Down
152 changes: 101 additions & 51 deletions log/logtest/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package logtest // import "go.opentelemetry.io/otel/log/logtest"
import (
"context"
"sync"
"time"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/log"
Expand Down Expand Up @@ -59,126 +60,175 @@ func NewRecorder(options ...Option) *Recorder {
}
}

// 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
// SchemaURL of the telemetry emitted by the scope.
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

ctx context.Context
}
// Record represents the record alongside its context.
type Record struct {
// Ensure forward compatibility by explicitly making this not comparable.
_ [0]func()

// Context provides the context emitted with the record.
func (rwc EmittedRecord) Context() context.Context {
return rwc.ctx
Context context.Context
EventName string
Timestamp time.Time
ObservedTimestamp time.Time
Severity log.Severity
SeverityText string
Body log.Value
Attributes []log.KeyValue
}

// Recorder is a recorder that stores all received log records
// in-memory.
// 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 []*logger
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)

// 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 {
// Reset clears the in-memory log records for all loggers.
func (r *Recorder) Reset() {
r.mu.Lock()
defer r.mu.Unlock()

ret := []*ScopeRecords{}
for _, l := range r.loggers {
ret = append(ret, l.scopeRecord)
l.Reset()
}
return ret
}

// Reset clears the in-memory log records for all loggers.
func (r *Recorder) Reset() {
// 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()

for _, l := range r.loggers {
l.Reset()
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 res
}

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.
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.
func (l *logger) Reset() {
l.mu.Lock()
defer l.mu.Unlock()

l.scopeRecord.Records = nil
l.records = nil
}
Loading
Loading