Skip to content

Commit

Permalink
feat(otelslog): add WithSource option
Browse files Browse the repository at this point in the history
Add a slogbridge option to include the source file location in the log attributes
  • Loading branch information
Jesse0Michael committed Oct 15, 2024
1 parent c3c8538 commit 04e5ef5
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 1 deletion.
28 changes: 27 additions & 1 deletion bridges/otelslog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"context"
"fmt"
"log/slog"
"runtime"
"slices"

"go.opentelemetry.io/otel/log"
Expand All @@ -64,6 +65,7 @@ type config struct {
provider log.LoggerProvider
version string
schemaURL string
source bool
}

func newConfig(options []Option) config {
Expand Down Expand Up @@ -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 {
Expand All @@ -140,6 +151,8 @@ type Handler struct {
attrs *kvBuffer
group *group
logger log.Logger

source bool
}

// Compile-time check *Handler implements slog.Handler.
Expand All @@ -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.
Expand All @@ -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()...)
}
Expand Down
55 changes: 55 additions & 0 deletions bridges/otelslog/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ func TestNewHandlerConfiguration(t *testing.T) {
WithLoggerProvider(r),
WithVersion("ver"),
WithSchemaURL("url"),
WithSource(true),
)
})
require.NotNil(t, h.logger)
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit 04e5ef5

Please sign in to comment.