diff --git a/bridges/otellogr/example_test.go b/bridges/otellogr/example_test.go index 4b544a0b2b5..e8858a7521f 100644 --- a/bridges/otellogr/example_test.go +++ b/bridges/otellogr/example_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-logr/logr" "go.opentelemetry.io/contrib/bridges/otellogr" + "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/noop" ) @@ -17,6 +18,17 @@ func Example() { // Create an logr.Logger with *otellogr.LogSink and use it in your application. logr.New(otellogr.NewLogSink( "my/pkg/name", - otellogr.WithLoggerProvider(provider)), - ) + otellogr.WithLoggerProvider(provider), + // Optionally, set the log level severity mapping. + otellogr.WithLevelSeverity(func(level int) log.Severity { + switch level { + case 0: + return log.SeverityInfo + case 1: + return log.SeverityDebug + default: + return log.SeverityTrace + } + }), + )) } diff --git a/bridges/otellogr/logsink.go b/bridges/otellogr/logsink.go index 482314c8719..4e36fe18e23 100644 --- a/bridges/otellogr/logsink.go +++ b/bridges/otellogr/logsink.go @@ -10,13 +10,21 @@ // way: // // - Message is set as the Body using a [log.StringValue]. -// - TODO: Level +// - Level is transformed and set as the Severity. The SeverityText is not +// set. // - KeyAndValues are transformed and set as Attributes. // - 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 // is used. // +// The V-level is transformed by using the [WithLevelSeverity] option. If option is +// not provided then V-level is transformed in the following way: +// +// - logr.Info and logr.V(0) are transformed to [log.SeverityInfo]. +// - logr.V(1) is transformed to [log.SeverityDebug]. +// - logr.V(2) and higher are transformed to [log.SeverityTrace]. +// // KeysAndValues values are transformed based on their type. The following types are // supported: // @@ -57,6 +65,8 @@ type config struct { provider log.LoggerProvider version string schemaURL string + + levelSeverity func(int) log.Severity } func newConfig(options []Option) config { @@ -69,6 +79,19 @@ func newConfig(options []Option) config { c.provider = global.GetLoggerProvider() } + if c.levelSeverity == nil { + c.levelSeverity = func(level int) log.Severity { + switch level { + case 0: + return log.SeverityInfo + case 1: + return log.SeverityDebug + default: + return log.SeverityTrace + } + } + } + return c } @@ -113,6 +136,22 @@ func WithLoggerProvider(provider log.LoggerProvider) Option { }) } +// WithLevelSeverity returns an [Option] that configures the function used to +// convert logr levels to OpenTelemetry log severities. +// +// By default if this Option is not provided, the LogSink will use a default +// conversion function that transforms in the following way: +// +// - logr.Info and logr.V(0) are transformed to [log.SeverityInfo]. +// - logr.V(1) is transformed to [log.SeverityDebug]. +// - logr.V(2) and higher are transformed to [log.SeverityTrace]. +func WithLevelSeverity(f func(int) log.Severity) Option { + return optFunc(func(c config) config { + c.levelSeverity = f + return c + }) +} + // NewLogSink returns a new [LogSink] to be used as a [logr.LogSink]. // // If [WithLoggerProvider] is not provided, the returned [LogSink] will use the @@ -129,10 +168,11 @@ func NewLogSink(name string, options ...Option) *LogSink { } return &LogSink{ - name: name, - provider: c.provider, - logger: c.provider.Logger(name, opts...), - opts: opts, + name: name, + provider: c.provider, + logger: c.provider.Logger(name, opts...), + levelSeverity: c.levelSeverity, + opts: opts, } } @@ -142,12 +182,13 @@ type LogSink struct { // Ensure forward compatibility by explicitly making this not comparable. noCmp [0]func() //nolint: unused // This is indeed used. - name string - provider log.LoggerProvider - logger log.Logger - opts []log.LoggerOption - attr []log.KeyValue - ctx context.Context + name string + provider log.LoggerProvider + logger log.Logger + levelSeverity func(int) log.Severity + opts []log.LoggerOption + attr []log.KeyValue + ctx context.Context } // Compile-time check *Handler implements logr.LogSink. @@ -157,8 +198,10 @@ var _ logr.LogSink = (*LogSink)(nil) // For example, commandline flags might be used to set the logging // verbosity and disable some info logs. func (l *LogSink) Enabled(level int) bool { - // TODO - return true + var param log.EnabledParameters + param.SetSeverity(l.levelSeverity(level)) + ctx := context.Background() + return l.logger.Enabled(ctx, param) } // Error logs an error, with the given message and key/value pairs. @@ -170,7 +213,7 @@ func (l *LogSink) Error(err error, msg string, keysAndValues ...any) { func (l *LogSink) Info(level int, msg string, keysAndValues ...any) { var record log.Record record.SetBody(log.StringValue(msg)) - record.SetSeverity(log.SeverityInfo) // TODO: level + record.SetSeverity(l.levelSeverity(level)) record.AddAttributes(l.attr...) diff --git a/bridges/otellogr/logsink_test.go b/bridges/otellogr/logsink_test.go index b0ba3446b1a..b63a201c412 100644 --- a/bridges/otellogr/logsink_test.go +++ b/bridges/otellogr/logsink_test.go @@ -64,7 +64,9 @@ func TestNewConfig(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantConfig, newConfig(tt.options)) + config := newConfig(tt.options) + config.levelSeverity = nil // Ignore asserting level severity function, assert.Equal does not support function comparison + assert.Equal(t, tt.wantConfig, config) }) } } @@ -117,9 +119,10 @@ func TestLogSink(t *testing.T) { const name = "name" for _, tt := range []struct { - name string - f func(*logr.Logger) - wantRecords map[string][]log.Record + name string + f func(*logr.Logger) + levelSeverity func(int) log.Severity + wantRecords map[string][]log.Record }{ { name: "no_log", @@ -139,6 +142,48 @@ func TestLogSink(t *testing.T) { }, }, }, + { + name: "info_with_level_severity", + f: func(l *logr.Logger) { + l.V(0).Info("msg") + l.V(1).Info("msg") + l.V(2).Info("msg") + l.V(3).Info("msg") + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityInfo, nil), + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityDebug, nil), + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityTrace, nil), + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityTrace, nil), + }, + }, + }, + { + name: "info_with_custom_level_severity", + f: func(l *logr.Logger) { + l.Info("msg") + l.V(1).Info("msg") + l.V(2).Info("msg") + }, + levelSeverity: func(level int) log.Severity { + switch level { + case 1: + return log.SeverityError + case 2: + return log.SeverityWarn + default: + return log.SeverityInfo + } + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityInfo, nil), + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityError, nil), + buildRecord(log.StringValue("msg"), time.Time{}, log.SeverityWarn, nil), + }, + }, + }, { name: "info_multi_attrs", f: func(l *logr.Logger) { @@ -235,7 +280,10 @@ func TestLogSink(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { rec := logtest.NewRecorder() - ls := NewLogSink(name, WithLoggerProvider(rec)) + ls := NewLogSink(name, + WithLoggerProvider(rec), + WithLevelSeverity(tt.levelSeverity), + ) l := logr.New(ls) tt.f(&l) @@ -260,6 +308,33 @@ func TestLogSink(t *testing.T) { } } +func TestLogSinkEnabled(t *testing.T) { + enabledFunc := func(ctx context.Context, param log.EnabledParameters) bool { + lvl, ok := param.Severity() + if !ok { + return true + } + return lvl == log.SeverityInfo + } + + rec := logtest.NewRecorder(logtest.WithEnabledFunc(enabledFunc)) + ls := NewLogSink( + "name", + WithLoggerProvider(rec), + WithLevelSeverity(func(i int) log.Severity { + switch i { + case 0: + return log.SeverityInfo + default: + return log.SeverityDebug + } + }), + ) + + assert.True(t, ls.Enabled(0)) + assert.False(t, ls.Enabled(1)) +} + func buildRecord(body log.Value, timestamp time.Time, severity log.Severity, attrs []log.KeyValue) log.Record { var record log.Record record.SetBody(body)