Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

otelslog: Add WithSource option #6253

Merged
merged 11 commits into from
Oct 18, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion bridges/otelslog/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
Expand Down
29 changes: 28 additions & 1 deletion bridges/otelslog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -64,6 +66,7 @@ type config struct {
provider log.LoggerProvider
version string
schemaURL string
source bool
}

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

source bool
}

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