From e40bcc07e52b168e657940afe2b2d58f6398fa55 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 3 Aug 2023 13:47:20 +0200 Subject: [PATCH 1/3] test with recent Go --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4d17f2f..340460b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,7 +9,7 @@ jobs: test: strategy: matrix: - version: [ '1.15', '1.16', '1.17', '1.18' ] + version: [ '1.15', '1.16', '1.17', '1.18', '1.19', '1.20', '1.21.0-rc.4' ] platform: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: From b142419d7cce7bf56ceed5e9467ba6ab020e83c6 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 3 Aug 2023 13:40:56 +0200 Subject: [PATCH 2/3] support slog This enables the use of the logr.Logger API on top of a slog.Handler. The other direction is also possible, but some information is lost. Instead, logr implementations should better get extended for usage as slog.Handler. Going back and forth between the two APIs is now part of logr. --- README.md | 83 +++++++++++++++++++++++++++++++++++++- slog.go | 81 +++++++++++++++++++++++++++++++++++++ slog_test.go | 72 +++++++++++++++++++++++++++++++++ sloghandler.go | 71 +++++++++++++++++++++++++++++++++ slogsink.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 slog.go create mode 100644 slog_test.go create mode 100644 sloghandler.go create mode 100644 slogsink.go diff --git a/README.md b/README.md index 72aefa3..58aa40b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,28 @@ received: If the Go standard library had defined an interface for logging, this project probably would not be needed. Alas, here we are. +When the Go developers started developing such an interface with +[slog](https://github.com/golang/go/issues/56345), they adopted some of the +logr design but also left out some parts and changed others: + +| Feature | logr | slog | +|---------|------|------| +| High-level API | `Logger` (passed by value) | `Logger` (passed by [pointer](https://github.com/golang/go/issues/59126)) | +| Low-level API | `LogSink` | `Handler` | +| Stack unwinding | done by `LogSink` | done by `Logger` | +| Skipping helper functions | `WithCallDepth`, `WithCallStackHelper` | [not supported by Logger](https://github.com/golang/go/issues/59145) | +| Generating a value for logging on demand | `Marshaler` | `LogValuer` | +| Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" | +| Error log entries | always logged, don't have a verbosity level | normal log entries with level >= `LevelError` | +| Passing logger via context | `NewContext`, `FromContext` | no API | +| Adding a name to a logger | `WithName` | no API | +| Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` | + +The high-level slog API is explicitly meant to be one of many different APIs +that can be layered on top of a shared `slog.Handler`. logr is one such +alternative API, with interoperability as [described +below](#slog-interoperability). + ### Inspiration Before you consider this package, please read [this blog post by the @@ -119,6 +141,63 @@ There are implementations for the following logging libraries: - **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0) - **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing) +## slog interoperability + +Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler` +and using the `slog.Logger` API with a `logr.LogSink`. logr provides `ToSlog` and +`FromSlog` API calls to convert between a `logr.Logger` and a `slog.Logger`. Because +the `slog.Logger` API is optional, there are also variants of these calls which +work directly with a `slog.Handler`. + +Ideally, the backend should support both logr and slog. In that case, log calls +can go from the high-level API to the backend with no intermediate glue +code. Because of a conflict in the parameters of the common Enabled method, it +is [not possible to implement both interfaces in the same +type](https://github.com/golang/go/issues/59110). A second type and methods for +converting from one type to the other are needed. Here is an example: + +``` +// logSink implements logr.LogSink and logr.SlogImplementor. +type logSink struct { ... } + +func (l *logSink) Enabled(lvl int) bool { ... } +... + +// logHandler implements slog.Handler. +type logHandler logSink + +func (l *logHandler) Enabled(ctx context.Context, slog.Level) bool { ... } +... + +// Explicit support for converting between the two types is needed by logr +// because it cannot do type assertions. + +func (l *logSink) GetSlogHandler() slog.Handler { return (*logHandler)(l) } +func (l *logHandler) GetLogrLogSink() logr.LogSink { return (*logSink)(l) } +``` + +Such a backend also should support values that implement specific interfaces +from both packages for logging (`logr.Marshaler`, `slog.LogValuer`). logr does not +convert between those. + +If a backend only supports `logr.LogSink`, then `ToSlog` uses +[`slogHandler`](sloghandler.go) to implement the `logr.Handler` on top of that +`logr.LogSink`. This solution is problematic because there is no way to log the +correct call site. All log entries with `slog.Level` >= `slog.LevelInfo` (= 0) +and < `slog.LevelError` get logged as info message with logr level 0, >= +`slog.LevelError` as error message and negative levels as debug messages with +negated level (i.e. `slog.LevelDebug` = -4 becomes +`V(4).Info`). `slog.LogValuer` will not get used. Applications which care about +these aspects should switch to a logr implementation which supports slog. + +If a backend only supports slog.Handler, then `FromSlog` uses +[`slogSink`](slogsink.go). This solution is more viable because call sites can +be logged correctly. However, `logr.Marshaler` will not get used. Types that +support `logr.Marshaler` should also support +`slog.LogValuer`. `logr.Logger.Error` logs with `slog.ErrorLevel`, +`logr.Logger.Info` with the negated level (i.e. `V(0).Info` uses `slog.Level` 0 += `slog.InfoLevel`, `V(4).Info` uses `slog.Level` -4 = `slog.DebugLevel`). + ## FAQ ### Conceptual @@ -242,7 +321,9 @@ Otherwise, you can start out with `0` as "you always want to see this", Then gradually choose levels in between as you need them, working your way down from 10 (for debug and trace style logs) and up from 1 (for chattier -info-type logs.) +info-type logs). For reference, slog pre-defines -4 for debug logs +(corresponds to 4 in logr), which matches what is +[recommended for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use). #### How do I choose my keys? diff --git a/slog.go b/slog.go new file mode 100644 index 0000000..cac1247 --- /dev/null +++ b/slog.go @@ -0,0 +1,81 @@ +//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 logr + +import ( + "log/slog" +) + +// SlogImplementor is an interface that a logr.LogSink can implement +// to support efficient logging through the slog.Logger API. +type SlogImplementor interface { + // GetSlogHandler returns a handler which uses the same settings as the logr.LogSink. + GetSlogHandler() slog.Handler +} + +// LogrImplementor is an interface that a slog.Handler can implement +// to support efficient logging through the logr.Logger API. +type LogrImplementor interface { + // GetLogrLogSink returns a sink which uses the same settings as the slog.Handler. + GetLogrLogSink() LogSink +} + +// ToSlog returns a slog.Logger which writes to the same backend as the logr.Logger. +func ToSlog(logger Logger) *slog.Logger { + return slog.New(ToSlogHandler(logger)) +} + +// ToSlog returns a slog.Handler which writes to the same backend as the logr.Logger. +func ToSlogHandler(logger Logger) slog.Handler { + if slogImplementor, ok := logger.GetSink().(SlogImplementor); ok { + handler := slogImplementor.GetSlogHandler() + return handler + } + + return &slogHandler{sink: logger.GetSink()} +} + +// FromSlog returns a logr.Logger which writes to the same backend as the slog.Logger. +func FromSlog(logger *slog.Logger) Logger { + return FromSlogHandler(logger.Handler()) +} + +// FromSlog returns a logr.Logger which writes to the same backend as the slog.Handler. +func FromSlogHandler(handler slog.Handler) Logger { + if logrImplementor, ok := handler.(LogrImplementor); ok { + logSink := logrImplementor.GetLogrLogSink() + return New(logSink) + } + + return New(&slogSink{handler: handler}) +} + +func levelFromSlog(level slog.Level) int { + if level >= 0 { + // logr has no level lower than 0, so we have to truncate. + return 0 + } + return int(-level) +} + +func levelToSlog(level int) slog.Level { + // logr starts at info = 0 and higher values go towards debugging (negative in slog). + return slog.Level(-level) +} diff --git a/slog_test.go b/slog_test.go new file mode 100644 index 0000000..d95b2a7 --- /dev/null +++ b/slog_test.go @@ -0,0 +1,72 @@ +//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 logr_test + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/funcr" +) + +var debugWithoutTime = &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == "time" { + return slog.Attr{} + } + return a + }, + Level: slog.LevelDebug, +} + +func ExampleFromSlog() { + logger := logr.FromSlog(slog.New(slog.NewTextHandler(os.Stdout, debugWithoutTime))) + + logger.Info("hello world") + logger.Error(errors.New("fake error"), "ignore me") + logger.WithValues("x", 1, "y", 2).WithValues("str", "abc").WithName("foo").WithName("bar").V(4).Info("with values, verbosity and name") + + // Output: + // level=INFO msg="hello world" + // level=ERROR msg="ignore me" err="fake error" + // level=DEBUG msg="foo/bar: with values, verbosity and name" x=1 y=2 str=abc +} + +func ExampleToSlog() { + logger := logr.ToSlog(funcr.New(func(prefix, args string) { + if prefix != "" { + fmt.Fprintln(os.Stdout, prefix, args) + } else { + fmt.Fprintln(os.Stdout, args) + } + }, funcr.Options{})) + + logger.Info("hello world") + logger.Error("ignore me", "err", errors.New("fake error")) + logger.With("x", 1, "y", 2).WithGroup("group").With("str", "abc").Warn("with values and group") + + // Output: + // "level"=0 "msg"="hello world" + // "msg"="ignore me" "error"=null "err"="fake error" + // "level"=0 "msg"="with values and group" "x"=1 "y"=2 "group.str"="abc" +} diff --git a/sloghandler.go b/sloghandler.go new file mode 100644 index 0000000..8df9118 --- /dev/null +++ b/sloghandler.go @@ -0,0 +1,71 @@ +//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 logr + +import ( + "context" + "log/slog" +) + +type slogHandler struct { + sink LogSink + groupPrefix string +} + +func (l *slogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return l.sink.Enabled(levelFromSlog(level)) +} + +func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error { + kvList := make([]any, 0, 2*record.NumAttrs()) + record.Attrs(func(attr slog.Attr) bool { + kvList = append(kvList, appendPrefix(l.groupPrefix, attr.Key), attr.Value.Any()) + return true + }) + if record.Level >= slog.LevelError { + l.sink.Error(nil, record.Message, kvList...) + } else { + l.sink.Info(levelFromSlog(record.Level), record.Message, kvList...) + } + return nil +} + +func (l slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + kvList := make([]any, 0, 2*len(attrs)) + for _, attr := range attrs { + kvList = append(kvList, appendPrefix(l.groupPrefix, attr.Key), attr.Value.Any()) + } + l.sink = l.sink.WithValues(kvList...) + return &l +} + +func (l slogHandler) WithGroup(name string) slog.Handler { + l.groupPrefix = appendPrefix(l.groupPrefix, name) + return &l +} + +func appendPrefix(prefix, name string) string { + if prefix == "" { + return name + } + return prefix + "." + name +} + +var _ slog.Handler = &slogHandler{} diff --git a/slogsink.go b/slogsink.go new file mode 100644 index 0000000..253d042 --- /dev/null +++ b/slogsink.go @@ -0,0 +1,106 @@ +//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 logr + +import ( + "context" + "log/slog" + "runtime" + "time" +) + +type slogSink struct { + callDepth int + prefix string + handler slog.Handler +} + +func (l *slogSink) Init(info RuntimeInfo) { + l.callDepth = info.CallDepth +} + +func (l *slogSink) WithCallDepth(depth int) LogSink { + newLogger := *l + newLogger.callDepth += depth + return &newLogger +} + +func (l *slogSink) Enabled(level int) bool { + return l.handler.Enabled(context.Background(), levelToSlog(level)) +} + +func (l *slogSink) Info(level int, msg string, kvList ...interface{}) { + if l.prefix != "" { + msg = l.prefix + ": " + msg + } + record := slog.NewRecord(time.Now(), levelToSlog(level), msg, l.infoOrErrorCaller()) + record.Add(kvList...) + l.handler.Handle(context.Background(), record) +} + +func (l *slogSink) Error(err error, msg string, kvList ...interface{}) { + if l.prefix != "" { + msg = l.prefix + ": " + msg + } + record := slog.NewRecord(time.Now(), slog.LevelError, msg, l.infoOrErrorCaller()) + if err != nil { + record.Add("err", err) + } + record.Add(kvList...) + l.handler.Handle(context.Background(), record) +} + +func (l *slogSink) WithName(name string) LogSink { + new := *l + if l.prefix != "" { + new.prefix = l.prefix + "/" + } + new.prefix += name + return &new +} + +func (l *slogSink) WithValues(kvList ...interface{}) LogSink { + new := *l + new.handler = l.handler.WithAttrs(kvListToAttrs(kvList...)) + return &new +} + +// infoOrErrorCaller is a helper for Info and Error. +func (l *slogSink) infoOrErrorCaller() uintptr { + var pcs [1]uintptr + // skip [runtime.Callers, this function, Info/Error, and all helper functions above that. + runtime.Callers(4+l.callDepth, pcs[:]) + return pcs[0] +} + +func kvListToAttrs(kvList ...interface{}) []slog.Attr { + // We don't need the record itself, only its Add method. + record := slog.NewRecord(time.Time{}, 0, "", 0) + record.Add(kvList...) + attrs := make([]slog.Attr, 0, record.NumAttrs()) + record.Attrs(func(attr slog.Attr) bool { + attrs = append(attrs, attr) + return true + }) + return attrs +} + +var _ LogSink = &slogSink{} +var _ CallDepthLogSink = &slogSink{} From ddaa1acc4cc860790fcf9bccfcd4ceeb40e1a712 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 3 Aug 2023 22:05:38 +0200 Subject: [PATCH 3/3] support slog.Logger and slog.Handler in context The same key must be used to ensure that the most recent logger gets retrieved. Converting on retrieval has the advantage that clients that only use one API don't need conversion. --- context.go | 31 ++++++++++++ context_noslog.go | 52 ++++++++++++++++++++ context_slog.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++ logr.go | 43 ---------------- 4 files changed, 206 insertions(+), 43 deletions(-) create mode 100644 context.go create mode 100644 context_noslog.go create mode 100644 context_slog.go diff --git a/context.go b/context.go new file mode 100644 index 0000000..4b3d979 --- /dev/null +++ b/context.go @@ -0,0 +1,31 @@ +/* +Copyright 2019 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 logr + +// contextKey is how we find loggers in a context.Context. +type contextKey struct{} + +// notFoundError exists to carry an IsNotFound method. +type notFoundError struct{} + +func (notFoundError) Error() string { + return "no logger was present" +} + +func (notFoundError) IsNotFound() bool { + return true +} diff --git a/context_noslog.go b/context_noslog.go new file mode 100644 index 0000000..b4a7d30 --- /dev/null +++ b/context_noslog.go @@ -0,0 +1,52 @@ +//go:build !go1.21 +// +build !go1.21 + +/* +Copyright 2019 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 logr + +import ( + "context" +) + +// This file contains the version of NewContext and FromContext which supports +// only storing and retrieving a Logger. + +// FromContext returns a Logger from ctx or an error if no Logger is found. +func FromContext(ctx context.Context) (Logger, error) { + if v, ok := ctx.Value(contextKey{}).(Logger); ok { + return v, nil + } + + return Logger{}, notFoundError{} +} + +// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this +// returns a Logger that discards all log messages. +func FromContextOrDiscard(ctx context.Context) Logger { + if v, ok := ctx.Value(contextKey{}).(Logger); ok { + return v + } + + return Discard() +} + +// NewContext returns a new Context, derived from ctx, which carries the +// provided Logger. +func NewContext(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} diff --git a/context_slog.go b/context_slog.go new file mode 100644 index 0000000..fa708ea --- /dev/null +++ b/context_slog.go @@ -0,0 +1,123 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2019 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 logr + +import ( + "context" + "log/slog" +) + +// This file contains the version of NewContext and FromContext which supports +// storing different types of loggers and converts as needed when retrieving +// the most recent one. + +// FromContext returns a Logger from ctx or an error if no Logger is found. +func FromContext(ctx context.Context) (Logger, error) { + l := ctx.Value(contextKey{}) + + switch l := l.(type) { + case Logger: + return l, nil + case *slog.Logger: + return FromSlog(l), nil + case slog.Handler: + return FromSlogHandler(l), nil + } + + return Logger{}, notFoundError{} +} + +// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this +// returns a Logger that discards all log messages. +func FromContextOrDiscard(ctx context.Context) Logger { + l, err := FromContext(ctx) + if err != nil { + return Discard() + } + return l +} + +// SlogFromContext is a variant of FromContext that returns a slog.Logger. +func SlogFromContext(ctx context.Context) (*slog.Logger, error) { + l := ctx.Value(contextKey{}) + + switch l := l.(type) { + case Logger: + return ToSlog(l), nil + case *slog.Logger: + return l, nil + case slog.Handler: + return slog.New(l), nil + } + + return nil, notFoundError{} +} + +// SlogFromContextOrDiscard is a variant of FromContextOrDiscard that returns a slog.Logger. +func SlogFromContextOrDiscard(ctx context.Context) *slog.Logger { + l, err := SlogFromContext(ctx) + if err != nil { + return ToSlog(Discard()) // TODO: use something simpler + } + return l +} + +// SlogHandlerFromContext is a variant of FromContext that returns a slog.Handler. +func SlogHandlerFromContext(ctx context.Context) (slog.Handler, error) { + l := ctx.Value(contextKey{}) + + switch l := l.(type) { + case Logger: + return ToSlogHandler(l), nil + case *slog.Logger: + return l.Handler(), nil + case slog.Handler: + return l, nil + } + + return nil, notFoundError{} +} + +// SlogHandlerFromContextOrDiscard is a variant of FromContextOrDiscard that returns a slog.Handler. +func SlogHandlerFromContextOrDiscard(ctx context.Context) slog.Handler { + l, err := SlogHandlerFromContext(ctx) + if err != nil { + return ToSlog(Discard()).Handler() // TODO: use something simpler + } + return l +} + +// NewContext returns a new Context, derived from ctx, which carries the +// provided Logger. +func NewContext(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} + +// SlogNewContext returns a new Context, derived from ctx, which carries the +// provided slog.Logger. +func SlogNewContext(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} + +// SlogHandlerNewContext returns a new Context, derived from ctx, which carries the +// provided slog.Handler. +func SlogHandlerNewContext(ctx context.Context, handler slog.Handler) context.Context { + return context.WithValue(ctx, contextKey{}, handler) +} diff --git a/logr.go b/logr.go index cc8d955..75a38b2 100644 --- a/logr.go +++ b/logr.go @@ -207,10 +207,6 @@ limitations under the License. // those. package logr -import ( - "context" -) - // New returns a new Logger instance. This is primarily used by libraries // implementing LogSink, rather than end users. Passing a nil sink will create // a Logger which discards all log lines. @@ -397,45 +393,6 @@ func (l Logger) IsZero() bool { return l.sink == nil } -// contextKey is how we find Loggers in a context.Context. -type contextKey struct{} - -// FromContext returns a Logger from ctx or an error if no Logger is found. -func FromContext(ctx context.Context) (Logger, error) { - if v, ok := ctx.Value(contextKey{}).(Logger); ok { - return v, nil - } - - return Logger{}, notFoundError{} -} - -// notFoundError exists to carry an IsNotFound method. -type notFoundError struct{} - -func (notFoundError) Error() string { - return "no logr.Logger was present" -} - -func (notFoundError) IsNotFound() bool { - return true -} - -// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this -// returns a Logger that discards all log messages. -func FromContextOrDiscard(ctx context.Context) Logger { - if v, ok := ctx.Value(contextKey{}).(Logger); ok { - return v - } - - return Discard() -} - -// NewContext returns a new Context, derived from ctx, which carries the -// provided Logger. -func NewContext(ctx context.Context, logger Logger) context.Context { - return context.WithValue(ctx, contextKey{}, logger) -} - // RuntimeInfo holds information that the logr "core" library knows which // LogSinks might want to know. type RuntimeInfo struct {