diff --git a/bridges/otellogr/go.mod b/bridges/otellogr/go.mod index 07acd113f5c..1dd3c721013 100644 --- a/bridges/otellogr/go.mod +++ b/bridges/otellogr/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/go-logr/logr v1.4.2 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/log v0.8.0 ) @@ -12,7 +13,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/bridges/otellogr/logsink.go b/bridges/otellogr/logsink.go index 4e36fe18e23..b0861dfe387 100644 --- a/bridges/otellogr/logsink.go +++ b/bridges/otellogr/logsink.go @@ -13,6 +13,8 @@ // - Level is transformed and set as the Severity. The SeverityText is not // set. // - KeyAndValues are transformed and set as Attributes. +// - Error is always logged as an additional attribute with the key +// "exception.message" and with the severity [log.SeverityError]. // - The [context.Context] value in KeyAndValues is propagated to OpenTelemetry // log record. All non-nested [context.Context] values are ignored and not // added as attributes. If there are multiple [context.Context] the last one @@ -59,6 +61,7 @@ import ( "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" ) type config struct { @@ -206,7 +209,20 @@ func (l *LogSink) Enabled(level int) bool { // Error logs an error, with the given message and key/value pairs. func (l *LogSink) Error(err error, msg string, keysAndValues ...any) { - // TODO + var record log.Record + record.SetBody(log.StringValue(msg)) + record.SetSeverity(log.SeverityError) + + record.AddAttributes( + log.String(string(semconv.ExceptionMessageKey), err.Error()), + ) + + record.AddAttributes(l.attr...) + + ctx, attr := convertKVs(l.ctx, keysAndValues...) + record.AddAttributes(attr...) + + l.logger.Emit(ctx, record) } // Info logs a non-error message with the given key/value pairs. @@ -223,9 +239,12 @@ func (l *LogSink) Info(level int, msg string, keysAndValues ...any) { l.logger.Emit(ctx, record) } -// Init initializes the LogSink. +// Init receives optional information about the logr library this +// implementation does not use it. func (l *LogSink) Init(info logr.RuntimeInfo) { - // TODO + // We don't need to do anything here. + // CallDepth is used to calculate the caller's PC. + // PC is dropped as part of the conversion to the OpenTelemetry log.Record. } // WithName returns a new LogSink with the specified name appended. diff --git a/bridges/otellogr/logsink_test.go b/bridges/otellogr/logsink_test.go index b63a201c412..78d3bf491c4 100644 --- a/bridges/otellogr/logsink_test.go +++ b/bridges/otellogr/logsink_test.go @@ -4,6 +4,7 @@ package otellogr import ( "context" + "errors" "testing" "time" @@ -277,6 +278,49 @@ func TestLogSink(t *testing.T) { }, }, }, + { + name: "error", + f: func(l *logr.Logger) { + l.Error(errors.New("test"), "error message") + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue("error message"), time.Time{}, log.SeverityError, []log.KeyValue{ + {Key: "exception.message", Value: log.StringValue("test")}, + }), + }, + }, + }, + { + name: "error_multi_attrs", + f: func(l *logr.Logger) { + l.Error(errors.New("test error"), "msg", + "struct", struct{ data int64 }{data: 1}, + "bool", true, + "duration", time.Minute, + "float64", 3.14159, + "int64", -2, + "string", "str", + "time", time.Unix(1000, 1000), + "uint64", uint64(3), + ) + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityError, []log.KeyValue{ + {Key: "exception.message", Value: log.StringValue("test error")}, + log.String("struct", "{data:1}"), + log.Bool("bool", true), + log.Int64("duration", 60_000_000_000), + log.Float64("float64", 3.14159), + log.Int64("int64", -2), + log.String("string", "str"), + log.Int64("time", time.Unix(1000, 1000).UnixNano()), + log.Int64("uint64", 3), + }), + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { rec := logtest.NewRecorder()