Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions v2/context.go
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,
}
}
98 changes: 98 additions & 0 deletions v2/context_test.go
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)
}
}
159 changes: 159 additions & 0 deletions v2/entry.go
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...)
}
3 changes: 3 additions & 0 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/containerd/log/v2

go 1.24
52 changes: 52 additions & 0 deletions v2/level.go
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
)
Loading