Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The next release will require at least [Go 1.25].
### Added

- Support testing of [Go 1.26]. (#7902)
- Add `SetError`/`GetError` on `Record` in `go.opentelemetry.io/otel/log` to attach an error and let the SDK record exception attributes. (#7924)

<!-- Released section -->
<!-- Don't change this section unless doing release -->
Expand Down
11 changes: 11 additions & 0 deletions log/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Record struct {
severity Severity
severityText string
body Value
err error

// The fields below are for optimizing the implementation of Attributes and
// AddAttributes. This design is borrowed from the slog Record type:
Expand Down Expand Up @@ -110,6 +111,16 @@ func (r *Record) SetBody(v Value) {
r.body = v
}

// Err returns the associated error if one has been set.
func (r *Record) Err() error {
return r.err
}

// SetErr sets the associated error. Passing nil clears the error.
func (r *Record) SetErr(err error) {
r.err = err
}

// WalkAttributes walks all attributes the log record holds by calling f for
// each on each [KeyValue] in the [Record]. Iteration stops if f returns false.
func (r *Record) WalkAttributes(f func(KeyValue) bool) {
Expand Down
49 changes: 49 additions & 0 deletions log/record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,52 @@ func TestRecordClone(t *testing.T) {
assert.Contains(t, r1Attrs, attr0)
assert.Contains(t, r1Attrs, attr1)
}

func TestSetErr(t *testing.T) {
err := assert.AnError

tests := []struct {
name string
setup func(*log.Record)
want error
same bool
}{
{
name: "zero value",
},
{
name: "set error",
setup: func(r *log.Record) {
r.SetErr(err)
},
want: err,
same: true,
},
{
name: "clear error",
setup: func(r *log.Record) {
r.SetErr(err)
r.SetErr(nil)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var r log.Record
if tt.setup != nil {
tt.setup(&r)
}

if tt.want == nil {
assert.NoError(t, r.Err())
return
}
if tt.same {
assert.Same(t, tt.want, r.Err())
return
}
assert.Equal(t, tt.want, r.Err())
})
}
}
87 changes: 87 additions & 0 deletions sdk/log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ package log // import "go.opentelemetry.io/otel/sdk/log"

import (
"context"
"reflect"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/log/embedded"
"go.opentelemetry.io/otel/sdk/instrumentation"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/trace"
)

var now = time.Now

const (
exceptionTypeKey = string(semconv.ExceptionTypeKey)
exceptionMessageKey = string(semconv.ExceptionMessageKey)
exceptionStacktraceKey = string(semconv.ExceptionStacktraceKey)
)

// Compile-time check logger implements log.Logger.
var _ log.Logger = (*logger)(nil)

Expand Down Expand Up @@ -108,10 +116,89 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record {
newRecord.observedTimestamp = now()
}

hasExceptionAttr := false
r.WalkAttributes(func(kv log.KeyValue) bool {
switch kv.Key {
case exceptionTypeKey, exceptionMessageKey, exceptionStacktraceKey:
hasExceptionAttr = true
}
newRecord.AddAttributes(kv)
return true
})

if !hasExceptionAttr {
addExceptionFromErrorNoScan(&newRecord, r.Err())
}

return newRecord
}

func addExceptionFromError(r *Record, err error) {
if r == nil || err == nil {
return
}

hasExceptionAttr := false
r.WalkAttributes(func(kv log.KeyValue) bool {
switch kv.Key {
case exceptionTypeKey, exceptionMessageKey, exceptionStacktraceKey:
hasExceptionAttr = true
return false
}
return true
})
if hasExceptionAttr {
return
}

addExceptionFromErrorNoScan(r, err)
}

func addExceptionFromErrorNoScan(r *Record, err error) {
if r == nil || err == nil {
return
}

var attrs [2]log.KeyValue
n := 0
if msg := err.Error(); msg != "" {
if r.attributeCountLimit > 0 && r.attributeCountLimit-r.AttributesLen() < 1 {
return
}
attrs[n] = log.String(exceptionMessageKey, msg)
n++
}
if errType := errorType(err); errType != "" {
if r.attributeCountLimit > 0 && r.attributeCountLimit-r.AttributesLen() < n+1 {
goto flush
}
attrs[n] = log.String(exceptionTypeKey, errType)
n++
}

flush:
if n > 0 {
r.addAttrs(attrs[:n])
}
}

func errorType(err error) string {
if et, ok := err.(interface{ ErrorType() string }); ok {
if s := et.ErrorType(); s != "" {
return s
}
}

t := reflect.TypeOf(err)
if t == nil {
return ""
}

pkg, name := t.PkgPath(), t.Name()
if pkg != "" && name != "" {
return pkg + "." + name
}

// This is not guaranteed to be unique, but is a best effort.
return t.String()
}
49 changes: 49 additions & 0 deletions sdk/log/logger_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package log // import "go.opentelemetry.io/otel/sdk/log"

import (
"errors"
"testing"
"time"

Expand All @@ -14,6 +15,7 @@ import (
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
)

func BenchmarkLoggerEmit(b *testing.B) {
Expand Down Expand Up @@ -119,6 +121,53 @@ func BenchmarkLoggerEnabled(b *testing.B) {
_ = enabled
}

func BenchmarkLoggerEmitExceptionAttributes(b *testing.B) {
logger := newTestLogger(b)

base := log.Record{}
base.SetBody(log.StringValue("boom"))
base.SetSeverity(log.SeverityError)

manualErr := errors.New("boom")
manual := base.Clone()
manual.AddAttributes(
log.String(string(semconv.ExceptionMessageKey), manualErr.Error()),
)

withErr := base.Clone()
withErr.SetErr(manualErr)

run := func(r log.Record) func(b *testing.B) {
return func(b *testing.B) {
ctx := b.Context()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Emit(ctx, r)
}
}
}

// Mimic otellogr logsink behavior: logger attributes + converted kv attrs.
baseAttrs := []log.KeyValue{
log.String("logger.name", "example"),
log.String("service.name", "svc"),
}
kvAttrs := []log.KeyValue{
log.String("key1", "value1"),
log.Int("key2", 2),
log.Bool("key3", true),
}

manual.AddAttributes(baseAttrs...)
manual.AddAttributes(kvAttrs...)
withErr.AddAttributes(baseAttrs...)
withErr.AddAttributes(kvAttrs...)

b.Run("Manual", run(manual))
b.Run("SetError", run(withErr))
}

func newTestLogger(t testing.TB) log.Logger {
provider := NewLoggerProvider(
WithProcessor(newFltrProcessor("0", false)),
Expand Down
Loading
Loading