vendor: github.com/containerd/log v0.2.0

full diff: https://github.com/containerd/log/compare/v0.1.0...v0.2.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-10-01 19:44:52 +02:00
parent 0932e7d9a8
commit 3caafba34f
10 changed files with 615 additions and 29 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+35 -15
View File
@@ -1,30 +1,50 @@
version: "2"
linters:
enable:
- exportloopref # Checks for pointers to enclosing loop variables
- gofmt
- goimports
- copyloopvar
- dupword
- gosec
- govet
- ineffassign
- misspell
- nolintlint
- revive
- staticcheck
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- unconvert
- unused
- vet
- dupword # Checks for duplicate words in the source code
disable:
- errcheck
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test
run:
timeout: 5m
skip-dirs:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test # e2e scripts
+61 -9
View File
@@ -40,8 +40,11 @@ package log
import (
"context"
"fmt"
"io"
"log/slog"
"github.com/sirupsen/logrus"
lslog "github.com/sirupsen/logrus/hooks/slog"
)
// G is a shorthand for [GetLogger].
@@ -108,10 +111,17 @@ const (
PanicLevel Level = logrus.PanicLevel
)
// SetLevel sets log level globally. It returns an error if the given
// level is not supported.
// levelValue is a log level accepted by [SetLevel].
type levelValue interface {
string | Level | slog.Level | lslog.Level | lslog.SlogLevel
}
// SetLevel sets the global log level.
//
// level can be one of:
// The level may be specified as a string, a [Level], a [slog.Level], an
// [lslog.Level], or an [lslog.SlogLevel].
//
// String levels are parsed using [logrus.ParseLevel] and may be one of:
//
// - "trace" ([TraceLevel])
// - "debug" ([DebugLevel])
@@ -120,10 +130,34 @@ const (
// - "error" ([ErrorLevel])
// - "fatal" ([FatalLevel])
// - "panic" ([PanicLevel])
func SetLevel(level string) error {
lvl, err := logrus.ParseLevel(level)
if err != nil {
return err
//
// slog levels are mapped to Logrus levels using the mapping defined by
// [lslog.SlogLevel]. [lslog.Level] values are accepted directly as their
// underlying Logrus level.
//
// SetLevel returns an error if a string level is not supported.
func SetLevel[T levelValue](level T) error {
var lvl Level
switch l := any(level).(type) {
case string:
var err error
lvl, err = logrus.ParseLevel(l)
if err != nil {
return err
}
case Level:
lvl = l
case slog.Level:
lvl = lslog.SlogLevel(l).Level()
case lslog.Level:
lvl = Level(l)
case lslog.SlogLevel:
lvl = l.Level()
}
L.Logger.SetLevel(lvl)
@@ -149,21 +183,39 @@ const (
// SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
func SetFormat(format OutputFormat) error {
if slogOut != nil {
var handler slog.Handler
switch format {
case TextFormat:
handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
case JSONFormat:
handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}})
default:
return fmt.Errorf("unknown log format: %s", format)
}
slog.SetDefault(slog.New(handler))
// Keep logrus formatting and output disabled, as both are handled by slog.
L.Logger.SetFormatter(discardFormatter{})
L.Logger.SetOutput(io.Discard)
return nil
}
switch format {
case TextFormat:
L.Logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat: RFC3339NanoFixed,
FullTimestamp: true,
})
return nil
case JSONFormat:
L.Logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: RFC3339NanoFixed,
})
return nil
default:
return fmt.Errorf("unknown log format: %s", format)
}
return nil
}
// WithLogger returns a new context with the provided logger. Use in
+66
View File
@@ -0,0 +1,66 @@
/*
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 (
"io"
"log/slog"
"sync"
"github.com/sirupsen/logrus"
lslog "github.com/sirupsen/logrus/hooks/slog"
)
// slogOut is used to set the slog logger when setting output format.
var slogOut io.Writer
// slogOnce guards UseSlog so repeated calls do not stack up hooks or
// reset slogOut to the discard writer installed on the first call.
var slogOnce sync.Once
func UseSlog() {
slogOnce.Do(func() {
L.Logger.SetNoLock()
L.Logger.AddHook(slogHook{})
slogOut = L.Logger.Out
// Disable logrus formatting and output, as both are handled by slog.
L.Logger.SetFormatter(discardFormatter{})
L.Logger.SetOutput(io.Discard)
})
}
type slogHook struct{}
func (slogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (slogHook) Fire(entry *logrus.Entry) error {
return lslog.NewHook(slog.Default(), nil).Fire(entry)
}
// loggerLevel exposes the current Logrus logger level as a slog.Leveler.
type loggerLevel struct{}
func (loggerLevel) Level() slog.Level {
return lslog.Level(L.Logger.GetLevel()).Level()
}
type discardFormatter struct{}
func (discardFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil }
+201
View File
@@ -0,0 +1,201 @@
package slog
import (
"context"
"log/slog"
"maps"
"runtime"
"slices"
"strings"
"github.com/sirupsen/logrus"
)
// HandlerOptions are options for a [Handler].
// A zero HandlerOptions consists entirely of default values.
type HandlerOptions struct {
// AddSource causes the handler to include the source code position
// of the log statement in the Logrus entry.
AddSource bool
// LevelMapper maps slog levels to Logrus levels. If nil, the default
// mapping is used. Set it to customize level mapping, for example to map
// custom slog levels to specific Logrus levels.
LevelMapper func(slog.Level) logrus.Level
}
// Handler is a [slog.Handler] that writes records to a [logrus.Logger].
//
// It is intended for bridging libraries or application code that log via slog
// into an existing Logrus logger, for example during a gradual migration.
//
// By default, slog levels are mapped using [SlogLevel].
// Set [HandlerOptions.LevelMapper] to customize the mapping.
//
// Mapping to [logrus.FatalLevel] or [logrus.PanicLevel] preserves the level
// only; handling a record does not exit or panic.
type Handler struct {
logger *logrus.Logger
opts HandlerOptions
// fields holds attributes from prior WithAttrs calls, already resolved
// under the group prefix that applied when they were added.
fields logrus.Fields
// groups is the current group name stack for attributes added later.
groups []string
}
var _ slog.Handler = (*Handler)(nil)
// NewHandler creates a [slog.Handler] that writes to the provided
// [logrus.Logger].
//
// The provided logger must not be nil. NewHandler panics if logger is nil.
// If opts is nil, the default options are used.
func NewHandler(logger *logrus.Logger, opts *HandlerOptions) *Handler {
if logger == nil {
panic("cannot create handler from nil logger")
}
if opts == nil {
opts = &HandlerOptions{}
}
return &Handler{
logger: logger,
opts: *opts,
}
}
// toLogrusLevel maps a slog level using LevelMapper or the default mapping.
func (h *Handler) toLogrusLevel(level slog.Level) logrus.Level {
if h.opts.LevelMapper != nil {
return h.opts.LevelMapper(level)
}
return SlogLevel(level).Level()
}
// Enabled reports whether the handler handles records at the given level.
// It maps the slog level to a logrus level and consults the underlying logger.
func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
return h.logger.IsLevelEnabled(h.toLogrusLevel(level))
}
// Handle converts the slog record into a logrus entry and logs it.
// Record time, context, message, and attributes (including those from
// [Handler.WithAttrs]/[Handler.WithGroup]) are preserved. Attributes are
// attached as logrus fields; group names are joined with "." as a key prefix
// (similar to [slog.TextHandler]).
func (h *Handler) Handle(ctx context.Context, record slog.Record) error {
// Stop recursive forwarding before it can cycle back through this logger.
if hasLogrusLogger(ctx, h.logger) {
return nil
}
level := h.toLogrusLevel(record.Level)
if !h.logger.IsLevelEnabled(level) {
return nil
}
entry := &logrus.Entry{
Logger: h.logger,
Data: h.fields,
Time: record.Time,
Context: ctx,
}
if h.opts.AddSource && record.PC != 0 {
// Preserve the caller selected by slog instead of rediscovering it
// through Logrus's caller stack filtering.
//
// Record.PC is a return PC; resolve it with CallersFrames.
frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next()
entry.Caller = &frame
}
if n := record.NumAttrs(); n > 0 {
// Clone before mutating the handler's fields.
entry.Data = maps.Clone(h.fields)
if entry.Data == nil {
entry.Data = make(logrus.Fields, n)
}
record.Attrs(func(a slog.Attr) bool {
appendAttr(entry.Data, h.groups, a)
return true
})
}
entry.Log(level, record.Message)
return nil
}
// WithAttrs returns a new Handler whose attributes consist of h's attributes
// followed by attrs.
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}
h2 := h.clone()
h2.fields = maps.Clone(h.fields)
if h2.fields == nil {
h2.fields = make(logrus.Fields, len(attrs))
}
for _, a := range attrs {
appendAttr(h2.fields, h.groups, a)
}
return h2
}
// WithGroup returns a new Handler with a group appended to the receiver's
// existing groups. All attributes added to the returned handler (via WithAttrs
// or Handle) are nested under the combined group names.
// If name is empty, WithGroup returns h unchanged.
func (h *Handler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
h2 := h.clone()
h2.groups = append(slices.Clip(h.groups), name)
return h2
}
// clone returns a shallow copy of h. Callers must clone fields or groups
// before modifying them.
func (h *Handler) clone() *Handler {
return &Handler{
logger: h.logger,
opts: h.opts,
fields: h.fields,
groups: h.groups,
}
}
// appendAttr adds attr to fields, resolving it and applying group prefixes.
// Group attributes are flattened with "."-separated keys.
func appendAttr(fields logrus.Fields, groups []string, attr slog.Attr) {
attr.Value = attr.Value.Resolve()
if attr.Equal(slog.Attr{}) {
return
}
if attr.Value.Kind() == slog.KindGroup {
gs := attr.Value.Group()
if len(gs) == 0 {
return
}
g := groups
if attr.Key != "" {
g = append(slices.Clip(groups), attr.Key)
}
for _, a := range gs {
appendAttr(fields, g, a)
}
return
}
key := attr.Key
if len(groups) > 0 {
key = strings.Join(groups, ".")
if attr.Key != "" {
key += "." + attr.Key
}
}
fields[key] = attr.Value.Any()
}
+119
View File
@@ -0,0 +1,119 @@
package slog
import (
"log/slog"
"github.com/sirupsen/logrus"
)
// slog levels corresponding to Logrus levels.
const (
slogLevelTrace = slog.LevelDebug - 4
slogLevelDebug = slog.LevelDebug
slogLevelInfo = slog.LevelInfo
slogLevelWarn = slog.LevelWarn
slogLevelError = slog.LevelError
slogLevelFatal = slog.LevelError + 2
slogLevelPanic = slog.LevelError + 4
)
// A Leveler provides a [logrus.Level] value.
//
// It is the Logrus counterpart of [slog.Leveler].
//
// As [SlogLevel] itself implements Leveler, clients typically supply
// a SlogLevel value wherever a Leveler is needed.
// Clients who need to vary the level dynamically can provide a more complex
// Leveler implementation.
type Leveler interface {
Level() logrus.Level
}
// Level adapts a [logrus.Level] to a [slog.Leveler] using the default
// Logrus-to-slog level mapping:
//
// - [logrus.TraceLevel] -> [slog.LevelDebug] - 4
// - [logrus.DebugLevel] -> [slog.LevelDebug]
// - [logrus.InfoLevel] -> [slog.LevelInfo]
// - [logrus.WarnLevel] -> [slog.LevelWarn]
// - [logrus.ErrorLevel] -> [slog.LevelError]
// - [logrus.FatalLevel] -> [slog.LevelError] + 2
// - [logrus.PanicLevel] -> [slog.LevelError] + 4
//
// Unknown Logrus levels map to [slog.LevelError].
type Level logrus.Level
// Level returns l mapped to the corresponding slog level.
func (l Level) Level() slog.Level {
return toSlogLevel(logrus.Level(l))
}
var _ slog.Leveler = Level(0)
// SlogLevel adapts a [slog.Level] to a [Leveler] using the default
// slog-to-Logrus level mapping:
//
// - [slog.LevelDebug] - 4 -> [logrus.TraceLevel]
// - [slog.LevelDebug] -> [logrus.DebugLevel]
// - [slog.LevelInfo] -> [logrus.InfoLevel]
// - [slog.LevelWarn] -> [logrus.WarnLevel]
// - [slog.LevelError] -> [logrus.ErrorLevel]
// - [slog.LevelError] + 2 -> [logrus.FatalLevel]
// - [slog.LevelError] + 4 -> [logrus.PanicLevel]
//
// Levels between these boundaries map to the next lower Logrus severity.
// Levels below [slog.LevelDebug] map to [logrus.TraceLevel], and levels at or
// above the Panic boundary map to [logrus.PanicLevel].
type SlogLevel slog.Level
// Level returns l mapped to the corresponding Logrus level.
func (l SlogLevel) Level() logrus.Level {
return toLogrusLevel(slog.Level(l))
}
var _ Leveler = SlogLevel(0)
// toLogrusLevel maps a slog level using the default mapping.
func toLogrusLevel(level slog.Level) logrus.Level {
switch {
case level >= slogLevelPanic:
return logrus.PanicLevel
case level >= slogLevelFatal:
return logrus.FatalLevel
case level >= slogLevelError:
return logrus.ErrorLevel
case level >= slogLevelWarn:
return logrus.WarnLevel
case level >= slogLevelInfo:
return logrus.InfoLevel
case level >= slogLevelDebug:
return logrus.DebugLevel
case level >= slogLevelTrace:
return logrus.TraceLevel
default:
return logrus.TraceLevel
}
}
// toSlogLevel maps a Logrus level using the default mapping.
func toSlogLevel(level logrus.Level) slog.Level {
switch level {
case logrus.PanicLevel:
return slogLevelPanic
case logrus.FatalLevel:
return slogLevelFatal
case logrus.ErrorLevel:
return slogLevelError
case logrus.WarnLevel:
return slogLevelWarn
case logrus.InfoLevel:
return slogLevelInfo
case logrus.DebugLevel:
return slogLevelDebug
case logrus.TraceLevel:
return slogLevelTrace
default:
// Treat all unknown levels as errors
return slogLevelError
}
}
+126
View File
@@ -0,0 +1,126 @@
// Package slog provides adapters between Logrus and log/slog.
//
// [Handler] forwards slog records to a Logrus logger, while [Hook] forwards
// Logrus entries to a slog logger. They can be used independently to support
// incremental migration between the two logging APIs.
package slog
import (
"context"
"log/slog"
"maps"
"github.com/sirupsen/logrus"
)
// logrusLoggerContextKey is the context key used to track Logrus loggers a
// record has already traversed while being forwarded through slog adapters.
type logrusLoggerContextKey struct{}
// withLogrusLogger returns a context that records logger as having already
// handled the current log record.
func withLogrusLogger(ctx context.Context, logger *logrus.Logger) context.Context {
seen, _ := ctx.Value(logrusLoggerContextKey{}).(map[*logrus.Logger]struct{})
seen = maps.Clone(seen)
if seen == nil {
seen = make(map[*logrus.Logger]struct{}, 1)
}
seen[logger] = struct{}{}
return context.WithValue(ctx, logrusLoggerContextKey{}, seen)
}
// hasLogrusLogger reports whether logger has already handled the current log
// record while it was forwarded through slog adapters.
func hasLogrusLogger(ctx context.Context, logger *logrus.Logger) bool {
seen, _ := ctx.Value(logrusLoggerContextKey{}).(map[*logrus.Logger]struct{})
_, ok := seen[logger]
return ok
}
// HookOptions are options for a [Hook].
// A zero HookOptions consists entirely of default values.
type HookOptions struct {
// LevelMapper maps Logrus levels to slog levels. If nil, the default
// mapping is used. Set it to customize level mapping, for example to map
// custom Logrus levels to specific slog levels.
LevelMapper func(logrus.Level) slog.Level
}
// Hook sends Logrus entries to slog.
//
// It is intended for bridging libraries or application code that log via
// Logrus into an existing slog logger, for example during a gradual migration.
//
// By default, Logrus levels are mapped using [Level]. Set
// [HookOptions.LevelMapper] to customize the mapping.
type Hook struct {
logger *slog.Logger
opts HookOptions
}
var _ logrus.Hook = (*Hook)(nil)
// NewHook creates a [logrus.Hook] that sends logs to the provided [slog.Logger].
//
// This hook is intended to be used during transition from Logrus to slog,
// or as a shim between different parts of your application or different
// libraries that depend on different loggers.
//
// The provided logger must not be nil. NewHook panics if logger is nil.
// If opts is nil, the default options are used.
func NewHook(logger *slog.Logger, opts *HookOptions) *Hook {
if logger == nil {
panic("cannot create hook from nil logger")
}
if opts == nil {
opts = &HookOptions{}
}
return &Hook{
logger: logger,
opts: *opts,
}
}
// toSlogLevel maps a Logrus level using LevelMapper or the default mapping.
func (h *Hook) toSlogLevel(level logrus.Level) slog.Level {
if h.opts.LevelMapper != nil {
return h.opts.LevelMapper(level)
}
return Level(level).Level()
}
// Levels always returns all levels, since slog allows controlling level
// enabling based on context.
func (h *Hook) Levels() []logrus.Level {
return logrus.AllLevels
}
// Fire forwards the provided logrus Entry to the underlying slog.Logger's
// Handler, mapping it to a slog.Record. Time and caller information are
// preserved when available, and Entry.Data is converted to attributes.
// If Entry.Context is nil, context.Background() is used.
func (h *Hook) Fire(entry *logrus.Entry) error {
ctx := entry.Context
if ctx == nil {
ctx = context.Background()
}
// Track this logger to guard against recursive forwarding.
ctx = withLogrusLogger(ctx, entry.Logger)
level := h.toSlogLevel(entry.Level)
handler := h.logger.Handler()
if !handler.Enabled(ctx, level) {
return nil
}
attrs := make([]slog.Attr, 0, len(entry.Data))
for k, v := range entry.Data {
attrs = append(attrs, slog.Any(k, v))
}
var pc uintptr
if entry.Caller != nil {
pc = entry.Caller.PC
}
r := slog.NewRecord(entry.Time, level, entry.Message, pc)
r.AddAttrs(attrs...)
return handler.Handle(ctx, r)
}