diff --git a/log/internal/global/log.go b/log/internal/global/log.go index d97ee966350..76269ed684a 100644 --- a/log/internal/global/log.go +++ b/log/internal/global/log.go @@ -102,6 +102,20 @@ func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool return enabled } +func (l *logger) EmitEvent(ctx context.Context, eventName string, e log.Event) { + if del, ok := l.delegate.Load().(log.Logger); ok { + del.EmitEvent(ctx, eventName, e) + } +} + +func (l *logger) EnabledEvent(ctx context.Context, eventName string, param log.EnabledEventParameters) bool { + var enabled bool + if del, ok := l.delegate.Load().(log.Logger); ok { + enabled = del.EnabledEvent(ctx, eventName, param) + } + return enabled +} + func (l *logger) setDelegate(provider log.LoggerProvider) { l.delegate.Store(provider.Logger(l.name, l.options...)) } diff --git a/log/internal/global/log_test.go b/log/internal/global/log_test.go index ae2c5b2a6dd..7f702ace53a 100644 --- a/log/internal/global/log_test.go +++ b/log/internal/global/log_test.go @@ -99,7 +99,7 @@ func (p *testLoggerProvider) Logger(name string, _ ...log.LoggerOption) log.Logg } type testLogger struct { - embedded.Logger + log.Logger emitN, enabledN int } diff --git a/log/logger.go b/log/logger.go index 0773a49b608..a562d04de04 100644 --- a/log/logger.go +++ b/log/logger.go @@ -33,8 +33,8 @@ type Logger interface { // Is should not be used for writing instrumentation. Emit(ctx context.Context, record Record) - // Enabled returns whether the Logger emits for the given context and - // param. + // Enabled returns whether the Logger emits a log record for the given + // context and param. // // The passed param is likely to be a partial record with only the // bridge-relevant information being provided (e.g a param with only the @@ -57,6 +57,36 @@ type Logger interface { // Notice: Enabled is intended to be used by log bridges. // Is should not be used for writing instrumentation. Enabled(ctx context.Context, param EnabledParameters) bool + + // EmitEvent emits an event. + // + // The event may be held by the implementation. Callers should not mutate + // the event after passed. + // + // Implementations of this method need to be safe for a user to call + // concurrently. + // + // Notice: EmitEvent is intended to be used for writing instrumentation. + EmitEvent(ctx context.Context, eventName string, event Event) + + // EnabledEvent returns whether the Logger emits an event for the given + // context, eventName, and param. + // + // The returned value will be true when the Logger will emit for the + // provided context and param, and will be false if the Logger will not + // emit. The returned value may be true or false in an indeterminate state. + // An implementation should default to returning true for an indeterminate + // state, but may return false if valid reasons in particular circumstances + // exist (e.g. performance, correctness). + // + // The param should not be held by the implementation. A copy should be + // made if the record needs to be held after the call returns. + // + // Implementations of this method need to be safe for a user to call + // concurrently. + // + // Notice: EnabledEvent is intended to be used for writing instrumentation. + EnabledEvent(ctx context.Context, eventName string, param EnabledEventParameters) bool } // LoggerOption applies configuration options to a [Logger]. @@ -140,3 +170,8 @@ func WithSchemaURL(schemaURL string) LoggerOption { type EnabledParameters struct { Severity Severity } + +// EnabledEventParameters represents payload for [Logger]'s Enabled method. +type EnabledEventParameters struct { + Severity Severity +} diff --git a/log/logtest/recorder.go b/log/logtest/recorder.go index fd986c9afc4..3a7d4f4c39d 100644 --- a/log/logtest/recorder.go +++ b/log/logtest/recorder.go @@ -73,6 +73,10 @@ type ScopeRecords struct { // Records are the log records, and their associated context this // instrumentation scope recorded. Records []EmittedRecord + + // Events are the events, and their associated context this + // instrumentation scope recorded. + Events []EmittedEvent } // EmittedRecord holds a log record the instrumentation received, alongside its @@ -88,6 +92,25 @@ func (rwc EmittedRecord) Context() context.Context { return rwc.ctx } +// EmittedEvent holds an event the instrumentation received, alongside its +// context and event name. +type EmittedEvent struct { + log.Event + + ctx context.Context + eventName string +} + +// Context provides the context emitted with the event. +func (rwc EmittedEvent) Context() context.Context { + return rwc.ctx +} + +// EventName provides the context emitted with the event. +func (rwc EmittedEvent) EventName() string { + return rwc.eventName +} + // Recorder is a recorder that stores all received log records // in-memory. type Recorder struct { @@ -182,3 +205,17 @@ func (l *logger) Reset() { l.scopeRecord.Records = nil } + +// Enabled indicates whether a specific event should be stored. +func (l *logger) EnabledEvent(ctx context.Context, eventName string, opts log.EnabledEventParameters) bool { + // TODO: + return true +} + +// EmitEvent stores the event. +func (l *logger) EmitEvent(ctx context.Context, eventName string, event log.Event) { + l.mu.Lock() + defer l.mu.Unlock() + + l.scopeRecord.Events = append(l.scopeRecord.Events, EmittedEvent{event, ctx, eventName}) +} diff --git a/log/noop/noop.go b/log/noop/noop.go index f45a7c7e0b3..4f850e065a1 100644 --- a/log/noop/noop.go +++ b/log/noop/noop.go @@ -48,3 +48,9 @@ func (Logger) Emit(context.Context, log.Record) {} // Enabled returns false. No log records are ever emitted. func (Logger) Enabled(context.Context, log.EnabledParameters) bool { return false } + +// EmitEvent does nothing. +func (Logger) EmitEvent(context.Context, string, log.Event) {} + +// EnabledEvent returns false. No events are ever emitted. +func (Logger) EnabledEvent(context.Context, string, log.EnabledEventParameters) bool { return false } diff --git a/sdk/log/logger.go b/sdk/log/logger.go index d6ca2ea41aa..e4b87071496 100644 --- a/sdk/log/logger.go +++ b/sdk/log/logger.go @@ -58,6 +58,31 @@ func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool return len(l.provider.processors) > len(fltrs) || anyEnabled(ctx, param, fltrs) } +func (l *logger) EmitEvent(ctx context.Context, eventName string, r log.Event) { + newRecord := l.newEvent(ctx, eventName, r) + for _, p := range l.provider.processors { + if err := p.OnEmit(ctx, &newRecord); err != nil { + otel.Handle(err) + } + } +} + +// EnabledEvent returns true if at least one Processor held by the LoggerProvider +// that created the logger will process param for the provided context and param. +// +// If it is not possible to definitively determine the param will be +// processed, true will be returned by default. A value of false will only be +// returned if it can be positively verified that no Processor will process. +func (l *logger) EnabledEvent(ctx context.Context, eventName string, param log.EnabledEventParameters) bool { + fltrs := l.provider.filterProcessors() + // If there are more Processors than FilterProcessors we cannot be sure + // that all Processors will drop the record. Therefore, return true. + // + // If all Processors are FilterProcessors, check if any is enabled. + + return len(l.provider.processors) > len(fltrs) || anyEnabled(ctx, log.EnabledParameters(param), fltrs) +} + func anyEnabled(ctx context.Context, param log.EnabledParameters, fltrs []x.FilterProcessor) bool { for _, f := range fltrs { if f.Enabled(ctx, param) { @@ -101,3 +126,38 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record { return newRecord } + +func (l *logger) newEvent(ctx context.Context, name string, e log.Event) Record { + sc := trace.SpanContextFromContext(ctx) + + newRecord := Record{ + eventName: name, + + timestamp: e.Timestamp(), + observedTimestamp: e.ObservedTimestamp(), + severity: e.Severity(), + severityText: e.SeverityText(), + body: e.Body(), + + traceID: sc.TraceID(), + spanID: sc.SpanID(), + traceFlags: sc.TraceFlags(), + + resource: l.provider.resource, + scope: &l.instrumentationScope, + attributeValueLengthLimit: l.provider.attributeValueLengthLimit, + attributeCountLimit: l.provider.attributeCountLimit, + } + + // This field SHOULD be set once the event is observed by OpenTelemetry. + if newRecord.observedTimestamp.IsZero() { + newRecord.observedTimestamp = now() + } + + e.WalkAttributes(func(kv log.KeyValue) bool { + newRecord.AddAttributes(kv) + return true + }) + + return newRecord +} diff --git a/sdk/log/record.go b/sdk/log/record.go index 155e4cad2b6..218c188b957 100644 --- a/sdk/log/record.go +++ b/sdk/log/record.go @@ -50,6 +50,8 @@ type Record struct { // Do not embed the log.Record. Attributes need to be overwrite-able and // deep-copying needs to be possible. + eventName string + timestamp time.Time observedTimestamp time.Time severity log.Severity @@ -104,6 +106,16 @@ func (r *Record) setDropped(n int) { r.dropped = n } +// EventName returns the event name. +func (r *Record) EventName() string { + return r.eventName +} + +// SetEventName sets the event name. +func (r *Record) SetEventName(s string) { + r.eventName = s +} + // Timestamp returns the time when the log record occurred. func (r *Record) Timestamp() time.Time { return r.timestamp