diff --git a/bridges/otelslog/handler.go b/bridges/otelslog/handler.go index 348cf81b1fa..277afc1550f 100644 --- a/bridges/otelslog/handler.go +++ b/bridges/otelslog/handler.go @@ -48,6 +48,7 @@ import ( "context" "fmt" "log/slog" + "runtime" "slices" "go.opentelemetry.io/otel/log" @@ -64,6 +65,7 @@ type config struct { provider log.LoggerProvider version string schemaURL string + source bool } func newConfig(options []Option) config { @@ -131,6 +133,15 @@ func WithLoggerProvider(provider log.LoggerProvider) Option { }) } +// WithSource returns an [Option] that configures the [log.Logger] to include +// the source location of the log record in log attributes. +func WithSource(source bool) Option { + return optFunc(func(c config) config { + c.source = source + return c + }) +} + // Handler is an [slog.Handler] that sends all logging records it receives to // OpenTelemetry. See package documentation for how conversions are made. type Handler struct { @@ -140,6 +151,8 @@ type Handler struct { attrs *kvBuffer group *group logger log.Logger + + source bool } // Compile-time check *Handler implements slog.Handler. @@ -155,7 +168,10 @@ var _ slog.Handler = (*Handler)(nil) // [log.Logger] implementation may override this value with a default. func NewHandler(name string, options ...Option) *Handler { cfg := newConfig(options) - return &Handler{logger: cfg.logger(name)} + return &Handler{ + logger: cfg.logger(name), + source: cfg.source, + } } // Handle handles the passed record. @@ -172,6 +188,16 @@ func (h *Handler) convertRecord(r slog.Record) log.Record { const sevOffset = slog.Level(log.SeverityDebug) - slog.LevelDebug record.SetSeverity(log.Severity(r.Level + sevOffset)) + if h.source { + fs := runtime.CallersFrames([]uintptr{r.PC}) + f, _ := fs.Next() + record.AddAttributes(log.Map("source", + log.String("function", f.Function), + log.String("file", f.File), + log.Int("line", f.Line)), + ) + } + if h.attrs.Len() > 0 { record.AddAttributes(h.attrs.KeyValues()...) } diff --git a/bridges/otelslog/handler_test.go b/bridges/otelslog/handler_test.go index 761889de2ee..10dd07d1970 100644 --- a/bridges/otelslog/handler_test.go +++ b/bridges/otelslog/handler_test.go @@ -455,6 +455,7 @@ func TestNewHandlerConfiguration(t *testing.T) { WithLoggerProvider(r), WithVersion("ver"), WithSchemaURL("url"), + WithSource(true), ) }) require.NotNil(t, h.logger) @@ -611,3 +612,57 @@ func BenchmarkHandler(b *testing.B) { _, _ = h, err } + +func TestHandler_convertRecord(t *testing.T) { + // Capture the PC of this line + pc, file, line, _ := runtime.Caller(0) + funcName := runtime.FuncForPC(pc).Name() + + tests := []struct { + name string + handler Handler + wantAttrs []log.KeyValue + }{ + { + name: "empty", + handler: Handler{}, + wantAttrs: []log.KeyValue{}, + }, + { + name: "with source", + handler: Handler{source: true}, + wantAttrs: []log.KeyValue{ + {Key: "source", Value: log.MapValue( + log.String("function", funcName), + log.String("file", file), + log.Int("line", line), + )}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slogRecord := slog.NewRecord(time.Now(), slog.LevelInfo, "body", pc) + record := tt.handler.convertRecord(slogRecord) + + // Validate attributes + attrMap := make(map[string]bool) + for _, attr := range tt.wantAttrs { + attrMap[attr.String()] = true + } + + record.WalkAttributes(func(kv log.KeyValue) bool { + if !attrMap[kv.String()] { + t.Errorf("Unexpected attribute: %v", kv) + return false + } + delete(attrMap, kv.String()) + return true + }) + + if len(attrMap) > 0 { + t.Errorf("Missing expected attributes: %v", attrMap) + } + }) + } +}