From 1c56a7cee97f5ce3a107f3c9bf9e6b31956f6780 Mon Sep 17 00:00:00 2001 From: Jesse Michael Date: Thu, 17 Oct 2024 21:41:24 -0700 Subject: [PATCH] otelslog: Add WithSource option (#6253) Add an otelslog option to include the source file location in the log attributes. Resolves https://github.com/open-telemetry/opentelemetry-go-contrib/issues/6244 --- CHANGELOG.md | 1 + bridges/otelslog/go.mod | 2 +- bridges/otelslog/handler.go | 29 ++++++++++++++++++++++++++++- bridges/otelslog/handler_test.go | 28 +++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d641da679c5..ab91768f636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Transform raw (`slog.KindAny`) attribute values to matching `log.Value` types. For example, `[]string{"foo", "bar"}` attribute value is now transformed to `log.SliceValue(log.StringValue("foo"), log.StringValue("bar"))` instead of `log.String("[foo bar"])`. (#6254) +- Add the `WithSource` option to the `go.opentelemetry.io/contrib/bridges/otelslog` log bridge to set the `code.*` attributes in the log record that includes the source location where the record was emitted. (#6253) ### Fixed diff --git a/bridges/otelslog/go.mod b/bridges/otelslog/go.mod index 3166cc845b3..5041613bf90 100644 --- a/bridges/otelslog/go.mod +++ b/bridges/otelslog/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/log v0.7.0 ) @@ -12,7 +13,6 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/bridges/otelslog/handler.go b/bridges/otelslog/handler.go index 1e252c88f70..6d40533a0b5 100644 --- a/bridges/otelslog/handler.go +++ b/bridges/otelslog/handler.go @@ -48,10 +48,12 @@ import ( "context" "fmt" "log/slog" + "runtime" "slices" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) // NewLogger returns a new [slog.Logger] backed by a new [Handler]. See @@ -64,6 +66,7 @@ type config struct { provider log.LoggerProvider version string schemaURL string + source bool } func newConfig(options []Option) config { @@ -131,6 +134,15 @@ func WithLoggerProvider(provider log.LoggerProvider) Option { }) } +// WithSource returns an [Option] that configures the [Handler] 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 +152,8 @@ type Handler struct { attrs *kvBuffer group *group logger log.Logger + + source bool } // Compile-time check *Handler implements slog.Handler. @@ -155,7 +169,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 +189,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.String(string(semconv.CodeFilepathKey), f.File), + log.String(string(semconv.CodeFunctionKey), f.Function), + log.Int(string(semconv.CodeLineNumberKey), 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 802774ff29f..9ca30603cdb 100644 --- a/bridges/otelslog/handler_test.go +++ b/bridges/otelslog/handler_test.go @@ -23,6 +23,7 @@ import ( "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" "go.opentelemetry.io/otel/log/global" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) var now = time.Now() @@ -149,6 +150,8 @@ type testCase struct { // checks is a list of checks to run on the result. Each item is a slice of // checks that will be evaluated for the corresponding record emitted. checks [][]check + // options are passed to the Handler constructed for this test case. + options []Option } // copied from slogtest (1.22.1). @@ -225,6 +228,10 @@ func (h *wrapper) Handle(ctx context.Context, r slog.Record) error { } func TestSLogHandler(t *testing.T) { + // Capture the PC of this line + pc, file, line, _ := runtime.Caller(0) + funcName := runtime.FuncForPC(pc).Name() + cases := []testCase{ { name: "Values", @@ -394,13 +401,31 @@ func TestSLogHandler(t *testing.T) { inGroup("G", missingKey("a")), }}, }, + { + name: "WithSource", + explanation: withSource("a Handler using the WithSource Option should include file attributes from where the log was emitted"), + f: func(l *slog.Logger) { + l.Info("msg") + }, + mod: func(r *slog.Record) { + // Assign the PC of record to the one captured above. + r.PC = pc + }, + checks: [][]check{{ + hasAttr(string(semconv.CodeFilepathKey), file), + hasAttr(string(semconv.CodeFunctionKey), funcName), + hasAttr(string(semconv.CodeLineNumberKey), int64(line)), + }}, + options: []Option{WithSource(true)}, + }, } // Based on slogtest.Run. for _, c := range cases { t.Run(c.name, func(t *testing.T) { r := new(recorder) - var h slog.Handler = NewHandler("", WithLoggerProvider(r)) + opts := append([]Option{WithLoggerProvider(r)}, c.options...) + var h slog.Handler = NewHandler("", opts...) if c.mod != nil { h = &wrapper{h, c.mod} } @@ -459,6 +484,7 @@ func TestNewHandlerConfiguration(t *testing.T) { WithLoggerProvider(r), WithVersion("ver"), WithSchemaURL("url"), + WithSource(true), ) }) require.NotNil(t, h.logger)