Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 6
Implement v2 interface based on slog#13
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
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| Copyright The containerd 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 log provides types and functions related to logging, passing | ||
| // loggers through a context, and attaching context to the logger. | ||
| // | ||
| // This package uses [log/slog] as the logging backend. It provides an | ||
| // [Entry] type for compatibility with code previously using logrus-style | ||
| // logging, while delegating all output to slog. | ||
| // | ||
| // Log level, format, and handler configuration should be done directly | ||
| // through [log/slog] using [slog.SetDefault] before using this package. | ||
| package log | ||
| import ( | ||
| "context" | ||
| "log/slog" | ||
| ) | ||
| // G is a shorthand for [GetLogger]. | ||
| // | ||
| // We may want to define this locally to a package to get package tagged log | ||
| // messages. | ||
| var G = GetLogger | ||
| // L is the default logger. | ||
| var L = &Entry{} | ||
| type loggerKey struct{} | ||
| // WithLogger returns a new context with the provided logger. Use in | ||
| // combination with logger.WithField(s) for great effect. | ||
| func WithLogger(ctx context.Context, logger *Entry) context.Context { | ||
| e := &Entry{ | ||
| logger: logger.logger, | ||
| attr: logger.attr, | ||
| ctx: ctx, | ||
| } | ||
| return context.WithValue(ctx, loggerKey{}, e) | ||
| } | ||
| // GetLogger retrieves the current logger from the context. If no logger is | ||
| // available, the default logger is returned. | ||
| func GetLogger(ctx context.Context) *Entry { | ||
| if logger := ctx.Value(loggerKey{}); logger != nil { | ||
| return logger.(*Entry) | ||
| } | ||
| return &Entry{ | ||
| logger: slog.Default(), | ||
| ctx: ctx, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| Copyright The containerd 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 log | ||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "testing" | ||
| ) | ||
| func TestLoggerContext(t *testing.T) { | ||
| ctx := context.Background() | ||
| ctx = WithLogger(ctx, G(ctx).WithField("test", "one")) | ||
| e := GetLogger(ctx) | ||
| a := G(ctx) | ||
| if e != a { | ||
| t.Errorf("should be the same entry: %+v, %+v", e, a) | ||
| } | ||
| } | ||
| func TestWithFields(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) | ||
| ctx := context.Background() | ||
| ctx = WithLogger(ctx, &Entry{logger: logger, ctx: ctx}) | ||
| l := G(ctx) | ||
| l = l.WithFields(Fields{"hello1": "world1"}) | ||
| l = l.WithFields(map[string]any{"hello2": "world2"}) | ||
| l.Info("test message") | ||
| output := buf.String() | ||
| for _, expected := range []string{"hello1=world1", "hello2=world2", "test message"} { | ||
| if !bytes.Contains([]byte(output), []byte(expected)) { | ||
| t.Errorf("expected output to contain %q, got: %s", expected, output) | ||
| } | ||
| } | ||
| } | ||
| func TestLogf(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: LevelTrace})) | ||
| ctx := context.Background() | ||
| ctx = WithLogger(ctx, &Entry{logger: logger, ctx: ctx}) | ||
| l := G(ctx) | ||
| l.Tracef("trace %s", "msg") | ||
| l.Debugf("debug %s", "msg") | ||
| l.Infof("info %s", "msg") | ||
| l.Warnf("warn %s", "msg") | ||
| l.Errorf("error %s", "msg") | ||
| output := buf.String() | ||
| for _, expected := range []string{"trace msg", "debug msg", "info msg", "warn msg", "error msg"} { | ||
| if !bytes.Contains([]byte(output), []byte(expected)) { | ||
| t.Errorf("expected output to contain %q, got: %s", expected, output) | ||
| } | ||
| } | ||
| } | ||
| func TestWithError(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| logger := slog.New(slog.NewTextHandler(&buf, nil)) | ||
| ctx := context.Background() | ||
| ctx = WithLogger(ctx, &Entry{logger: logger, ctx: ctx}) | ||
| l := G(ctx).WithError(fmt.Errorf("test error")) | ||
| l.Info("something failed") | ||
| output := buf.String() | ||
| if !bytes.Contains([]byte(output), []byte("error")) { | ||
| t.Errorf("expected output to contain error field, got: %s", output) | ||
| } | ||
| if !bytes.Contains([]byte(output), []byte("test error")) { | ||
| t.Errorf("expected output to contain error message, got: %s", output) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| /* | ||
| Copyright The containerd 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 log | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| ) | ||
| // Entry is a logging entry. It contains all the fields passed with | ||
| // [Entry.WithFields]. It's finally logged when Trace, Debug, Info, Warn, | ||
| // or Error is called on it. These objects can be reused and passed around | ||
| // as much as you wish to avoid field duplication. | ||
| // | ||
| // Entry is for close compatibility with logrus and containerd/log, | ||
| // consider using slog directly for new packages. | ||
| // | ||
| // NOTE: while similar, this package is not compatible with logrus | ||
| // or containerd/log. It should be compatible for most uses but | ||
| // the interface is reduced and only supports slog capabilities. | ||
| // Some notable differences: | ||
| // - No Fatal or Panic levels, no equivalent in slog | ||
| type Entry struct { | ||
| logger *slog.Logger | ||
| attr []slog.Attr | ||
| ctx context.Context | ||
| } | ||
| // Fields type to pass to "WithFields". | ||
| type Fields = map[string]any | ||
| // WithError adds an error as single field to the Entry. | ||
| func (entry *Entry) WithError(err error) *Entry { | ||
| return entry.WithField("error", err) | ||
| } | ||
| // WithContext adds a context to the Entry. | ||
| func (entry *Entry) WithContext(ctx context.Context) *Entry { | ||
| return &Entry{ | ||
| logger: entry.logger, | ||
| attr: entry.attr, | ||
| ctx: ctx, | ||
| } | ||
| } | ||
| // WithField adds a single field to the Entry. | ||
| func (entry *Entry) WithField(key string, value any) *Entry { | ||
| return entry.WithFields(Fields{key: value}) | ||
| } | ||
| // WithFields adds a map of fields to the Entry. | ||
| func (entry *Entry) WithFields(fields Fields) *Entry { | ||
| attr := make([]slog.Attr, len(entry.attr), len(entry.attr)+len(fields)) | ||
| copy(attr, entry.attr) | ||
| for k, v := range fields { | ||
| attr = append(attr, slog.Any(k, v)) | ||
| } | ||
| return &Entry{ | ||
| logger: entry.logger, | ||
| attr: attr, | ||
| ctx: entry.ctx, | ||
| } | ||
| } | ||
| // Log logs a message at the given level. | ||
| func (entry *Entry) Log(level slog.Level, msg string) { | ||
| logger := entry.logger | ||
| if logger == nil { | ||
| logger = slog.Default() | ||
| } | ||
| ctx := entry.ctx | ||
| if ctx == nil { | ||
| ctx = context.Background() | ||
| } | ||
| logger.LogAttrs(ctx, level, msg, entry.attr...) | ||
| ||
| } | ||
| // Logf logs a formatted message at the given level. | ||
| func (entry *Entry) Logf(level slog.Level, format string, args ...any) { | ||
| logger := entry.logger | ||
| if logger == nil { | ||
| logger = slog.Default() | ||
| } | ||
| if !logger.Enabled(entry.ctx, level) { | ||
| return | ||
| } | ||
| ctx := entry.ctx | ||
| if ctx == nil { | ||
| ctx = context.Background() | ||
| } | ||
| logger.LogAttrs(ctx, level, fmt.Sprintf(format, args...), entry.attr...) | ||
| } | ||
| // Trace logs a message at trace level (slog.LevelDebug-4). | ||
| func (entry *Entry) Trace(msg string) { | ||
| entry.Log(LevelTrace, msg) | ||
| } | ||
| // Tracef logs a formatted message at trace level. | ||
| func (entry *Entry) Tracef(format string, args ...any) { | ||
| entry.Logf(LevelTrace, format, args...) | ||
| } | ||
| // Debug logs a message at debug level. | ||
| func (entry *Entry) Debug(msg string) { | ||
| entry.Log(slog.LevelDebug, msg) | ||
| } | ||
| // Debugf logs a formatted message at debug level. | ||
| func (entry *Entry) Debugf(format string, args ...any) { | ||
| entry.Logf(slog.LevelDebug, format, args...) | ||
| } | ||
| // Info logs a message at info level. | ||
| func (entry *Entry) Info(msg string) { | ||
| entry.Log(slog.LevelInfo, msg) | ||
| } | ||
| // Infof logs a formatted message at info level. | ||
| func (entry *Entry) Infof(format string, args ...any) { | ||
| entry.Logf(slog.LevelInfo, format, args...) | ||
| } | ||
| // Warn logs a message at warn level. | ||
| func (entry *Entry) Warn(msg string) { | ||
| entry.Log(slog.LevelWarn, msg) | ||
| } | ||
| // Warnf logs a formatted message at warn level. | ||
| func (entry *Entry) Warnf(format string, args ...any) { | ||
| entry.Logf(slog.LevelWarn, format, args...) | ||
| } | ||
| // Error logs a message at error level. | ||
| func (entry *Entry) Error(msg string) { | ||
| entry.Log(slog.LevelError, msg) | ||
| } | ||
| // Errorf logs a formatted message at error level. | ||
| func (entry *Entry) Errorf(format string, args ...any) { | ||
| entry.Logf(slog.LevelError, format, args...) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| module github.com/containerd/log/v2 | ||
| go 1.24 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /* | ||
| Copyright The containerd 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 log | ||
| import "log/slog" | ||
| // Level is a logging level. | ||
| type Level = slog.Level | ||
| // Supported log levels. These correspond to slog levels, with additional | ||
| // levels defined for trace, fatal, and panic for compatibility. | ||
| const ( | ||
| // LevelTrace designates finer-grained informational events than | ||
| // [slog.LevelDebug]. | ||
| LevelTrace Level = slog.LevelDebug - 4 | ||
| // LevelDebug level. Usually only enabled when debugging. Very verbose | ||
| // logging. | ||
| LevelDebug Level = slog.LevelDebug | ||
| // LevelInfo level. General operational entries about what's going on | ||
| // inside the application. | ||
| LevelInfo Level = slog.LevelInfo | ||
| // LevelWarn level. Non-critical entries that deserve eyes. | ||
| LevelWarn Level = slog.LevelWarn | ||
| // LevelError level. Logs errors that should definitely be noted. | ||
| LevelError Level = slog.LevelError | ||
| // LevelFatal level. Provided for compatibility; maps to | ||
| // slog.LevelError + 2. | ||
| LevelFatal Level = slog.LevelError + 2 | ||
| // LevelPanic level. Provided for compatibility; maps to | ||
| // slog.LevelError + 4. | ||
| LevelPanic Level = slog.LevelError + 4 | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could consider lazy evaluation of logger here. i.e. leave logger as nil.
To be fair, the package doc clear states all log configuration should be done before package use, but in this case it's not too inconsequential to defer the evaluation. All the log functions already account for nil logger.