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 `source` attribute in the log record that includes the source location where the record was emitted. (#6253)
Jesse0Michael marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

Expand Down
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
pellared marked this conversation as resolved.
Show resolved Hide resolved
// 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",
Jesse0Michael marked this conversation as resolved.
Show resolved Hide resolved
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
25 changes: 24 additions & 1 deletion bridges/otelslog/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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 +227,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 +400,29 @@ func TestSLogHandler(t *testing.T) {
inGroup("G", missingKey("a")),
}},
},
{
name: "WithSource",
explanation: withSource("a Handler using the WithSource Option should include a source attribute containing the source location of where the file 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("source", map[string]any{"function": funcName, "file": file, "line": 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 +481,7 @@ func TestNewHandlerConfiguration(t *testing.T) {
WithLoggerProvider(r),
WithVersion("ver"),
WithSchemaURL("url"),
WithSource(true),
)
})
require.NotNil(t, h.logger)
Expand Down
Loading