Skip to content

Commit d884114

Browse files
committed
Unify logging with slog
Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent 01a9334 commit d884114

10 files changed

Lines changed: 635 additions & 44 deletions

File tree

cmd/containerd-shim-nerdbox-v1/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/containerd/containerd/v2/pkg/shim"
2323

24+
"github.com/containerd/nerdbox/internal/logging"
2425
"github.com/containerd/nerdbox/internal/shim/manager"
2526

2627
_ "github.com/containerd/nerdbox/plugins/shim/sandbox"
@@ -30,6 +31,14 @@ import (
3031
_ "github.com/containerd/nerdbox/plugins/vm/libkrun"
3132
)
3233

34+
func init() {
35+
logging.SetupShimLog()
36+
}
37+
3338
func main() {
34-
shim.RunShim(context.Background(), manager.NewShimManager("io.containerd.nerdbox.v1"))
39+
shim.RunShim(context.Background(), manager.NewShimManager("io.containerd.nerdbox.v1"),
40+
func(c *shim.Config) {
41+
c.NoSetupLogger = true
42+
},
43+
)
3544
}

cmd/vminitd/main.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"errors"
2424
"flag"
2525
"fmt"
26+
"log/slog"
2627
"net"
2728
"os"
2829
"os/signal"
@@ -55,6 +56,21 @@ import (
5556
_ "github.com/containerd/nerdbox/plugins/vminit/task"
5657
)
5758

59+
// logLevel controls the slog handler level for vminitd.
60+
var logLevel = &slog.LevelVar{}
61+
62+
func init() {
63+
log.UseSlog()
64+
// Write structured logs to /dev/console rather than stderr so that
65+
// output does not end up in the kernel message buffer (kmsg).
66+
console, err := os.OpenFile("/dev/console", os.O_WRONLY, 0644)
67+
if err != nil {
68+
console = os.Stderr
69+
}
70+
handler := slog.NewJSONHandler(console, &slog.HandlerOptions{Level: logLevel})
71+
slog.SetDefault(slog.New(handler).With("component", "vminitd"))
72+
}
73+
5874
func main() {
5975
t1 := time.Now()
6076
var (
@@ -74,18 +90,10 @@ func main() {
7490
}
7591
flag.CommandLine.Parse(args)
7692

77-
/*
78-
c, err := os.OpenFile("/dev/console", os.O_WRONLY, 0644)
79-
if err != nil {
80-
fmt.Fprintf(os.Stderr, "failed to open /dev/console: %v\n", err)
81-
os.Exit(1)
82-
}
83-
defer c.Close()
84-
log.L.Logger.SetOutput(c)
85-
*/
8693
var err error
8794

8895
if *dev || config.Debug {
96+
logLevel.Set(slog.LevelDebug)
8997
log.SetLevel("debug")
9098
}
9199

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/containerd/errdefs/pkg v0.3.0
1313
github.com/containerd/fifo v1.1.0
1414
github.com/containerd/go-runc v1.1.0
15-
github.com/containerd/log v0.1.0
15+
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b
1616
github.com/containerd/otelttrpc v0.1.0
1717
github.com/containerd/plugin v1.0.0
1818
github.com/containerd/ttrpc v1.2.8

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gG
2727
github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
2828
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
2929
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
30+
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b h1:VT47r68OzwhsTu84qAaG6Dv7xQVRmMvt7yotn9auLtI=
31+
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
3032
github.com/containerd/otelttrpc v0.1.0 h1:UOX68eVTE8H/T45JveIg+I22Ev2aFj4qPITCmXsskjw=
3133
github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg=
3234
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=

internal/logging/logging.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package logging provides unified structured logging utilities for the
18+
// shim and vminitd components.
19+
package logging
20+
21+
import (
22+
"bufio"
23+
"context"
24+
"encoding/json"
25+
"io"
26+
"log/slog"
27+
"os"
28+
"strings"
29+
"time"
30+
31+
"github.com/containerd/log"
32+
)
33+
34+
// SetupShimLog configures slog-based logging for the shim process.
35+
// It opens the platform-specific log output (FIFO on Unix, named pipe
36+
// on Windows), then creates a slog TextHandler and sets it as the
37+
// default logger with a "component=shim" attribute.
38+
//
39+
// The base handler (without component) is stored for use by
40+
// [ForwardConsoleLogs] so that forwarded records carry their own
41+
// component rather than inheriting "shim".
42+
//
43+
// For the short-lived start and delete actions, only [log.UseSlog] is
44+
// called to route logrus through slog; the log output is not opened.
45+
func SetupShimLog() {
46+
log.UseSlog()
47+
48+
var (
49+
debug bool
50+
ns string
51+
id string
52+
attrs []slog.Attr
53+
)
54+
args := os.Args[1:]
55+
for i := 0; i < len(args); i++ {
56+
switch args[i] {
57+
case "start", "delete":
58+
return
59+
case "-debug":
60+
debug = true
61+
case "-namespace":
62+
if i+1 < len(args) {
63+
i++
64+
ns = args[i]
65+
attrs = append(attrs, slog.String("ns", ns))
66+
}
67+
case "-id":
68+
if i+1 < len(args) {
69+
i++
70+
id = args[i]
71+
attrs = append(attrs, slog.String("id", id))
72+
}
73+
}
74+
}
75+
76+
w := openShimLog(ns, id)
77+
78+
var level slog.LevelVar
79+
if debug {
80+
level.Set(slog.LevelDebug)
81+
log.SetLevel("debug") //nolint:errcheck
82+
}
83+
84+
handler := slog.NewTextHandler(w, &slog.HandlerOptions{Level: &level}).WithAttrs(attrs)
85+
SetBaseHandler(handler)
86+
slog.SetDefault(slog.New(handler).With("component", "shim"))
87+
}
88+
89+
// baseHandler is the slog handler used by ForwardConsoleLogs to emit
90+
// records without the caller's pre-applied attributes (e.g. component=shim).
91+
var baseHandler slog.Handler
92+
93+
// SetBaseHandler stores the base handler for use by ForwardConsoleLogs.
94+
// This should be called before any console forwarding starts, typically
95+
// during init with the handler before any .With() attributes are applied.
96+
func SetBaseHandler(h slog.Handler) {
97+
baseHandler = h
98+
}
99+
100+
// consoleHandler returns the handler that ForwardConsoleLogs should use.
101+
// It prefers the base handler set via SetBaseHandler, falling back to
102+
// the default slog handler.
103+
func consoleHandler() slog.Handler {
104+
if baseHandler != nil {
105+
return baseHandler
106+
}
107+
return slog.Default().Handler()
108+
}
109+
110+
// ForwardConsoleLogs reads lines from r and re-emits them as structured
111+
// log entries through the base [slog.Handler] set via [SetBaseHandler].
112+
//
113+
// Lines that are valid JSON objects (emitted by vminitd's JSON slog handler)
114+
// are parsed and re-emitted preserving the original level, message,
115+
// and attributes. All other lines are treated as kernel messages and emitted
116+
// at INFO level with component=kmsg.
117+
//
118+
// The base handler is used directly (rather than the default logger) so that
119+
// pre-applied attributes such as component=shim are not added to forwarded
120+
// records, which carry their own component.
121+
//
122+
// If raw is non-nil, every line is also written there verbatim (useful for
123+
// tests that need the unprocessed console output).
124+
func ForwardConsoleLogs(r io.Reader, raw io.Writer) {
125+
scanner := bufio.NewScanner(r)
126+
for scanner.Scan() {
127+
line := scanner.Text()
128+
129+
if raw != nil {
130+
raw.Write([]byte(line))
131+
raw.Write([]byte("\n"))
132+
}
133+
134+
if line == "" {
135+
continue
136+
}
137+
138+
if strings.HasPrefix(line, "{") {
139+
if forwardJSONLog(line) {
140+
continue
141+
}
142+
}
143+
144+
// Kernel message — parse optional "[ 1.234567] " timestamp prefix.
145+
msg := line
146+
attrs := []slog.Attr{slog.String("component", "kmsg")}
147+
if after, ktime, ok := parseKernelTimestamp(line); ok {
148+
msg = after
149+
attrs = append(attrs, slog.String("ktime", ktime))
150+
}
151+
record := slog.NewRecord(time.Now(), slog.LevelInfo, msg, 0)
152+
record.AddAttrs(attrs...)
153+
handler := consoleHandler()
154+
if handler.Enabled(context.Background(), slog.LevelInfo) {
155+
handler.Handle(context.Background(), record) //nolint:errcheck
156+
}
157+
}
158+
if err := scanner.Err(); err != nil {
159+
record := slog.NewRecord(time.Now(), slog.LevelWarn, "console log reader stopped", 0)
160+
record.AddAttrs(slog.String("component", "kmsg"), slog.Any("error", err))
161+
handler := consoleHandler()
162+
if handler.Enabled(context.Background(), slog.LevelWarn) {
163+
handler.Handle(context.Background(), record) //nolint:errcheck
164+
}
165+
}
166+
}
167+
168+
// forwardJSONLog attempts to parse line as a JSON slog record and emit it
169+
// through the console handler. Returns true if the line was handled.
170+
func forwardJSONLog(line string) bool {
171+
var fields map[string]json.RawMessage
172+
if err := json.Unmarshal([]byte(line), &fields); err != nil {
173+
return false
174+
}
175+
176+
// A valid vminitd log must at least have "msg".
177+
rawMsg, ok := fields["msg"]
178+
if !ok {
179+
return false
180+
}
181+
182+
var msg string
183+
if err := json.Unmarshal(rawMsg, &msg); err != nil {
184+
return false
185+
}
186+
delete(fields, "msg")
187+
188+
var level slog.Level
189+
if raw, ok := fields["level"]; ok {
190+
var s string
191+
if err := json.Unmarshal(raw, &s); err == nil {
192+
level.UnmarshalText([]byte(s)) //nolint:errcheck
193+
}
194+
delete(fields, "level")
195+
}
196+
197+
// Discard the VM-side timestamp — the guest clock is not
198+
// synchronised and typically reads as epoch.
199+
delete(fields, "time")
200+
t := time.Now()
201+
202+
handler := consoleHandler()
203+
if !handler.Enabled(context.Background(), level) {
204+
return true
205+
}
206+
207+
record := slog.NewRecord(t, level, msg, 0)
208+
for k, v := range fields {
209+
var val any
210+
if err := json.Unmarshal(v, &val); err == nil {
211+
record.AddAttrs(slog.Any(k, val))
212+
}
213+
}
214+
215+
handler.Handle(context.Background(), record) //nolint:errcheck
216+
return true
217+
}
218+
219+
// parseKernelTimestamp extracts the "[ seconds.usecs] " prefix from a
220+
// kernel log line. Returns the message after the prefix, the timestamp
221+
// string, and whether a timestamp was found.
222+
func parseKernelTimestamp(line string) (msg, ktime string, ok bool) {
223+
if len(line) < 3 || line[0] != '[' {
224+
return "", "", false
225+
}
226+
end := strings.IndexByte(line, ']')
227+
if end < 0 {
228+
return "", "", false
229+
}
230+
ktime = strings.TrimSpace(line[1:end])
231+
msg = line[end+1:]
232+
if len(msg) > 0 && msg[0] == ' ' {
233+
msg = msg[1:]
234+
}
235+
return msg, ktime, true
236+
}

0 commit comments

Comments
 (0)