From ed9429f51973881d3d725b5b827c7190849bd9c7 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Thu, 3 Aug 2023 11:45:42 -0700 Subject: [PATCH 1/2] WIP: add slogr Not sure if this should be in logr or in a new repo, but I wanted to get feedback. No tests yet, and I am not sure how much testing this warrants. --- slogr/example/main.go | 65 ++++++++++++++++++++ slogr/slogr.go | 139 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 slogr/example/main.go create mode 100644 slogr/slogr.go diff --git a/slogr/example/main.go b/slogr/example/main.go new file mode 100644 index 0000000..bb6c575 --- /dev/null +++ b/slogr/example/main.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using funcr. +package main + +import ( + "log/slog" + "os" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/slogr" +) + +type e struct { + str string +} + +func (e e) Error() string { + return e.str +} + +func helper(log logr.Logger, msg string) { + helper2(log, msg) +} + +func helper2(log logr.Logger, msg string) { + log.WithCallDepth(2).Info(msg) +} + +func main() { + opts := slog.HandlerOptions{ + AddSource: true, + Level: slog.Level(-1), + } + handler := slog.NewJSONHandler(os.Stderr, &opts) + log := slogr.New(handler) + example(log) +} + +func example(log logr.Logger) { + log = log.WithName("my") + log = log.WithName("logger") + log = log.WithName("name") + log = log.WithValues("saved", "value") + log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.V(1).Info("2) you should see this") + log.V(1).V(1).Info("you should NOT see this") + log.Error(nil, "3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error(e{"an error occurred"}, "4) goodbye", "code", -1) + helper(log, "5) thru a helper") +} diff --git a/slogr/slogr.go b/slogr/slogr.go new file mode 100644 index 0000000..c16a96a --- /dev/null +++ b/slogr/slogr.go @@ -0,0 +1,139 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package slogr implements the github.com/go-logr/logr API in terms of Go's +// slog API. +package slogr + +import ( + "context" + "log/slog" + "runtime" + "time" + + "github.com/go-logr/logr" +) + +// New returns a logr.Logger which is implemented by an arbitrary slog Handler. +func New(handler slog.Handler) logr.Logger { + return logr.New(newSink(handler)) +} + +// Underlier exposes access to the underlying logging function. Since +// callers only have a logr.Logger, they have to know which +// implementation is in use, so this interface is less of an +// abstraction and more of a way to test type conversion. +type Underlier interface { + GetUnderlying() slog.Handler +} + +func newSink(handler slog.Handler) logr.LogSink { + sink := &slogSink{ + handler: handler, + } + // For skipping logr.Logger.Info and .Error. + return sink.WithCallDepth(1) +} + +// slogSink inherits some of its LogSink implementation from Formatter +// and just needs to add some glue code. +type slogSink struct { + handler slog.Handler + name string + depth int +} + +// Init configures this Formatter from runtime info, such as the call depth +// imposed by logr itself. +// Note that this receiver is a pointer, so depth can be saved. +func (sink *slogSink) Init(info logr.RuntimeInfo) { + sink.depth += info.CallDepth +} + +// Enabled checks whether an info message at the given level should be logged. +func (sink slogSink) Enabled(level int) bool { + return sink.handler.Enabled(context.Background(), slog.Level(-level)) +} + +func (sink slogSink) WithName(name string) logr.LogSink { + if len(sink.name) > 0 { + sink.name += "/" + } + sink.name += name + return &sink +} + +func (sink slogSink) WithValues(kvList ...interface{}) logr.LogSink { + r := slog.NewRecord(time.Time{}, 0, "", 0) + r.Add(kvList...) + attrs := make([]slog.Attr, 0, r.NumAttrs()) + r.Attrs(func(attr slog.Attr) bool { + attrs = append(attrs, attr) + return true + }) + sink.handler = sink.handler.WithAttrs(attrs) + return &sink +} + +func (sink slogSink) WithCallDepth(depth int) logr.LogSink { + sink.depth += depth + return &sink +} + +func (sink slogSink) Info(level int, msg string, kvList ...interface{}) { + args := make([]interface{}, 0, len(kvList)+4) + if len(sink.name) != 0 { + args = append(args, "logger", sink.name) + } + args = append(args, "vlevel", level) + args = append(args, kvList...) + + sink.log(slog.Level(-level), msg, args...) +} + +func (sink slogSink) Error(err error, msg string, kvList ...interface{}) { + args := make([]interface{}, 0, len(kvList)+4) + if len(sink.name) != 0 { + args = append(args, "logger", sink.name) + } + args = append(args, "err", err) + args = append(args, kvList...) + + sink.log(slog.LevelError, msg, args...) +} + +func (sink slogSink) log(level slog.Level, msg string, kvList ...interface{}) { + // TODO: Should this be optional via HandlerOptions? Literally + // HandlerOptions or something like it? + var pcs [1]uintptr + runtime.Callers(sink.depth+2, pcs[:]) // 2 = this frame and Info/Error + pc := pcs[0] + r := slog.NewRecord(time.Now(), level, msg, pc) + r.Add(kvList...) + sink.handler.Handle(context.Background(), r) +} + +func (sink slogSink) GetUnderlying() slog.Handler { + return sink.handler +} + +// Assert conformance to the interfaces. +var _ logr.LogSink = &slogSink{} +var _ logr.CallDepthLogSink = &slogSink{} +var _ Underlier = &slogSink{} From 2e4a2cfba0e5c892f88c9434153285b320782ba3 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Wed, 2 Aug 2023 13:26:55 -0700 Subject: [PATCH 2/2] WIP: add slogr.NewSlogHandler I am not sure this belongs in the same package as slogr. There are a few aspects of slog that logr has no equivalent for: - time passed in to the handler - passing in a PC rather than a call-depth - WithGroup() We need to think about whether to extend logr to accommodate or just ignore these. --- slogr/handler.go | 89 +++++++++++++++++++++++++ slogr/handler_example/main.go | 65 ++++++++++++++++++ slogr/{example => logr_example}/main.go | 0 slogr/slogr.go | 6 +- 4 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 slogr/handler.go create mode 100644 slogr/handler_example/main.go rename slogr/{example => logr_example}/main.go (100%) diff --git a/slogr/handler.go b/slogr/handler.go new file mode 100644 index 0000000..6b3bae9 --- /dev/null +++ b/slogr/handler.go @@ -0,0 +1,89 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package slogr + +import ( + "context" + "log/slog" + + "github.com/go-logr/logr" +) + +// NewSlogHandler returns a slog.Handler which logs through an arbitrary +// logr.Logger. +func NewSlogHandler(logger logr.Logger) slog.Handler { + return &logrHandler{ + // Account for extra wrapper functions. + logger: logger.WithCallDepth(3), + } +} + +// logrHandler implements slog.Handler in terms of a logr.Logger. +type logrHandler struct { + logger logr.Logger +} + +func (h logrHandler) Enabled(_ context.Context, level slog.Level) bool { + return h.logger.V(int(-level)).Enabled() +} + +func (h logrHandler) Handle(_ context.Context, record slog.Record) error { + //FIXME: I don't know what to do with record.Time or record.PC. Neither of + // them map to logr. + args := recordToKV(record) + if record.Level >= slog.LevelError { + h.logger.Error(nil, record.Message, args...) + } else { + h.logger.V(int(-record.Level)).Info(record.Message, args...) + } + return nil +} + +func recordToKV(record slog.Record) []any { + kv := make([]any, 0, record.NumAttrs()*2) + fn := func(attr slog.Attr) bool { + kv = append(kv, attr.Key, attr.Value.Any()) + return true + } + record.Attrs(fn) + return kv +} + +func attrsToKV(attrs []slog.Attr) []any { + kv := make([]any, 0, len(attrs)*2) + for _, attr := range attrs { + kv = append(kv, attr.Key, attr.Value.Any()) + } + return kv +} + +func (h logrHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + args := attrsToKV(attrs) + h.logger = h.logger.WithValues(args...) + return h +} + +func (h logrHandler) WithGroup(name string) slog.Handler { + //FIXME: I don't know how to implement this in logr, but it's an + //interesting idea + return h +} + +var _ slog.Handler = logrHandler{} diff --git a/slogr/handler_example/main.go b/slogr/handler_example/main.go new file mode 100644 index 0000000..2e67539 --- /dev/null +++ b/slogr/handler_example/main.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using funcr. +package main + +import ( + "context" + "fmt" + "log/slog" + + "github.com/go-logr/logr/funcr" + "github.com/go-logr/logr/slogr" +) + +type e struct { + str string +} + +func (e e) Error() string { + return e.str +} + +func helper(log *slog.Logger, msg string) { + helper2(log, msg) +} + +func helper2(log *slog.Logger, msg string) { + log.Info(msg) +} + +func main() { + logrLogger := funcr.New( + func(pfx, args string) { fmt.Println(pfx, args) }, + funcr.Options{ + LogCaller: funcr.All, + LogTimestamp: true, + Verbosity: 1, + }) + log := slog.New(slogr.NewSlogHandler(logrLogger)) + example(log) +} + +func example(log *slog.Logger) { + log = log.With("saved", "value") + log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.Log(context.TODO(), slog.Level(-1), "2) you should see this") + log.Log(context.TODO(), slog.Level(-2), "you should NOT see this") + log.Error("3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error("4) goodbye", "code", -1, "err", e{"an error occurred"}) + helper(log, "5) thru a helper") +} diff --git a/slogr/example/main.go b/slogr/logr_example/main.go similarity index 100% rename from slogr/example/main.go rename to slogr/logr_example/main.go diff --git a/slogr/slogr.go b/slogr/slogr.go index c16a96a..acd8cb7 100644 --- a/slogr/slogr.go +++ b/slogr/slogr.go @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package slogr implements the github.com/go-logr/logr API in terms of Go's -// slog API. +// Package slogr provides bridges between github.com/go-logr/logr and Go's +// slog package. package slogr import ( @@ -30,7 +30,7 @@ import ( "github.com/go-logr/logr" ) -// New returns a logr.Logger which is implemented by an arbitrary slog Handler. +// New returns a logr.Logger which logs through an arbitrary slog.Handler. func New(handler slog.Handler) logr.Logger { return logr.New(newSink(handler)) }