vendor: github.com/containerd/log v0.2.0 - #7347
Conversation
full diff: containerd/log@v0.1.0...v0.2.0 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
| key += "." + attr.Key | ||
| } | ||
| } | ||
| fields[key] = attr.Value.Any() |
There was a problem hiding this comment.
[medium] appendAttr stores non-group attrs with empty keys under the group prefix instead of skipping them
When a non-group slog.Attr has an empty key (attr.Key == "") but a non-zero value (e.g. slog.Any("", someValue)), the function does not skip it. Instead, when len(groups) > 0, key is set to strings.Join(groups, ".") — the bare group prefix — and then fields[key] = attr.Value.Any() is executed unconditionally. This silently stores the value under the group name key.
The Go standard library's slog.TextHandler and slog.JSONHandler both skip non-group attrs with empty keys, per the log/slog package specification. This implementation deviates from that behaviour: it may clobber an earlier value stored at the group prefix key, or introduce an unexpected field in the log output.
The fix is to add an early return for the empty-key non-group case, before the assignment:
| fields[key] = attr.Value.Any() | |
| if key == "" { | |
| return | |
| } | |
| fields[key] = attr.Value.Any() |
| Confidence | Score |
|---|---|
| 🟢 strong | 100/100 |
There was a problem hiding this comment.
Not sure if the empty key would make it so far here?
There was a problem hiding this comment.
Yeah, maybe not; I'll have a look in logrus though; seems like an easy fix to include in a future release.
There was a problem hiding this comment.
Actually; @docker-agent is wrong here; slog does explicitly allow empty keys, and ONLY ignore if an attribute is a zero-value; https://go.dev/src/log/slog/handler.go#L53
// - If an Attr's key and value are both the zero value, ignore the Attr.
// This can be tested with attr.Equal(Attr{}).
It even has an Attr.isEmpty it uses to check for that; https://go.dev/src/log/slog/handler.go#L487
https://go.dev/src/log/slog/attr.go#L106
// isEmpty reports whether a has an empty key and a nil value.
// That can be written as Attr{} or Any("", nil).
func (a Attr) isEmpty() bool {
return a.Key == "" && a.Value.num == 0 && a.Value.any == nil
}
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
| var slogOnce sync.Once | ||
|
|
||
| func UseSlog() { | ||
| slogOnce.Do(func() { |
There was a problem hiding this comment.
[medium] slogOut package variable written without synchronization between UseSlog and SetFormat
slogOut is a plain io.Writer interface variable (not an atomic pointer). It is written inside slogOnce.Do(...) in UseSlog (line 39) and read without any synchronization in SetFormat (context.go line 186, if slogOut != nil { ... }).
sync.Once guarantees that the function passed to Do completes before Do itself returns in any goroutine — but it does not synchronize a write to slogOut with a concurrent non-Do read in a different function (SetFormat). If a goroutine calls UseSlog() while another goroutine concurrently calls SetFormat(), the race detector will flag the write at line 39 and the read at context.go:186 as a data race under Go's memory model.
The fix is to guard slogOut with an atomic or a mutex. For example, using atomic.Pointer[io.Writer]:
var slogOut atomic.Pointer[io.Writer]Or protecting both the write and read with the same mutex already used elsewhere in the package.
In practice, UseSlog() is typically called once at startup before concurrent use begins, so the impact is low — but the race is real and the race detector will surface it.
| Confidence | Score |
|---|---|
| 🟢 strong | 97/100 |
|
|
||
| func UseSlog() { | ||
| slogOnce.Do(func() { | ||
| L.Logger.SetNoLock() |
There was a problem hiding this comment.
[medium] SetNoLock() is called before hooks and formatter are configured, creating a concurrent-logging window without the mutex
UseSlog() calls L.Logger.SetNoLock() as its first action (line 37), then proceeds to call AddHook, SetFormatter, and SetOutput on the now-unlocked logger. Between line 37 and the completion of the remaining setup, any concurrent goroutine that logs will:
- Proceed without the logrus mutex (it has been disabled), and
- Fire hooks and formatters that have not yet been configured to the slog state — the slog hook may not be installed yet, and the old formatter may still be active.
logrus.SetNoLock() is documented for use when the caller takes responsibility for external synchronization, or when it is called before any concurrent use begins. Here, UseSlog() can be called at any point during the program lifetime (it is guarded only against repeated calls via slogOnce, not against concurrent logging).
The safer ordering would be to configure the hook, formatter, and output first while the mutex is still active, and then call SetNoLock() last — or to hold an external lock for the full setup sequence.
| Confidence | Score |
|---|---|
| 🟡 moderate | 67/100 |
full diff: containerd/log@v0.1.0...v0.2.0
Summary
Release notes (optional)
A picture of a cute animal (not mandatory but encouraged)