Skip to content
Merged
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