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
11 changes: 10 additions & 1 deletion cmd/containerd-shim-nerdbox-v1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

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

"github.com/containerd/nerdbox/internal/logging"
"github.com/containerd/nerdbox/internal/shim/manager"

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

func init() {
logging.SetupShimLog()
}

func main() {
shim.RunShim(context.Background(), manager.NewShimManager("io.containerd.nerdbox.v1"))
shim.RunShim(context.Background(), manager.NewShimManager("io.containerd.nerdbox.v1"),
func(c *shim.Config) {
c.NoSetupLogger = true
},
)
}
26 changes: 17 additions & 9 deletions cmd/vminitd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"flag"
"fmt"
"log/slog"
"net"
"os"
"os/signal"
Expand Down Expand Up @@ -55,6 +56,21 @@ import (
_ "github.com/containerd/nerdbox/plugins/vminit/task"
)

// logLevel controls the slog handler level for vminitd.
var logLevel = &slog.LevelVar{}

func init() {
log.UseSlog()
// Write structured logs to /dev/console rather than stderr so that
// output does not end up in the kernel message buffer (kmsg).
console, err := os.OpenFile("/dev/console", os.O_WRONLY, 0644)
if err != nil {
console = os.Stderr
}
handler := slog.NewJSONHandler(console, &slog.HandlerOptions{Level: logLevel})
slog.SetDefault(slog.New(handler).With("component", "vminitd"))
}

func main() {
t1 := time.Now()
var (
Expand All @@ -74,18 +90,10 @@ func main() {
}
flag.CommandLine.Parse(args)

/*
c, err := os.OpenFile("/dev/console", os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open /dev/console: %v\n", err)
os.Exit(1)
}
defer c.Close()
log.L.Logger.SetOutput(c)
*/
var err error

if *dev || config.Debug {
logLevel.Set(slog.LevelDebug)
log.SetLevel("debug")
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/containerd/errdefs/pkg v0.3.0
github.com/containerd/fifo v1.1.0
github.com/containerd/go-runc v1.1.0
github.com/containerd/log v0.1.0
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b
github.com/containerd/otelttrpc v0.1.0
github.com/containerd/plugin v1.0.0
github.com/containerd/ttrpc v1.2.8
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA=
github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b h1:VT47r68OzwhsTu84qAaG6Dv7xQVRmMvt7yotn9auLtI=
github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/otelttrpc v0.1.0 h1:UOX68eVTE8H/T45JveIg+I22Ev2aFj4qPITCmXsskjw=
github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
Expand Down
236 changes: 236 additions & 0 deletions internal/logging/logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
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 logging provides unified structured logging utilities for the
// shim and vminitd components.
package logging

import (
"bufio"
"context"
"encoding/json"
"io"
"log/slog"
"os"
"strings"
"time"

"github.com/containerd/log"
)

// SetupShimLog configures slog-based logging for the shim process.
// It opens the platform-specific log output (FIFO on Unix, named pipe
// on Windows), then creates a slog TextHandler and sets it as the
// default logger with a "component=shim" attribute.
//
// The base handler (without component) is stored for use by
// [ForwardConsoleLogs] so that forwarded records carry their own
// component rather than inheriting "shim".
//
// For the short-lived start and delete actions, only [log.UseSlog] is
// called to route logrus through slog; the log output is not opened.
func SetupShimLog() {
log.UseSlog()

var (
debug bool
ns string
id string
attrs []slog.Attr
)
args := os.Args[1:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "start", "delete":
return
case "-debug":
debug = true
case "-namespace":
if i+1 < len(args) {
i++
ns = args[i]
attrs = append(attrs, slog.String("ns", ns))
}
case "-id":
if i+1 < len(args) {
i++
id = args[i]
attrs = append(attrs, slog.String("id", id))
}
}
}

w := openShimLog(ns, id)

var level slog.LevelVar
if debug {
level.Set(slog.LevelDebug)
log.SetLevel("debug") //nolint:errcheck
}

handler := slog.NewTextHandler(w, &slog.HandlerOptions{Level: &level}).WithAttrs(attrs)
SetBaseHandler(handler)
slog.SetDefault(slog.New(handler).With("component", "shim"))
}

// baseHandler is the slog handler used by ForwardConsoleLogs to emit
// records without the caller's pre-applied attributes (e.g. component=shim).
var baseHandler slog.Handler

// SetBaseHandler stores the base handler for use by ForwardConsoleLogs.
// This should be called before any console forwarding starts, typically
// during init with the handler before any .With() attributes are applied.
func SetBaseHandler(h slog.Handler) {
baseHandler = h
}

// consoleHandler returns the handler that ForwardConsoleLogs should use.
// It prefers the base handler set via SetBaseHandler, falling back to
// the default slog handler.
func consoleHandler() slog.Handler {
if baseHandler != nil {
return baseHandler
}
return slog.Default().Handler()
}

// ForwardConsoleLogs reads lines from r and re-emits them as structured
// log entries through the base [slog.Handler] set via [SetBaseHandler].
//
// Lines that are valid JSON objects (emitted by vminitd's JSON slog handler)
// are parsed and re-emitted preserving the original level, message,
// and attributes. All other lines are treated as kernel messages and emitted
// at INFO level with component=kmsg.
//
// The base handler is used directly (rather than the default logger) so that
// pre-applied attributes such as component=shim are not added to forwarded
// records, which carry their own component.
//
// If raw is non-nil, every line is also written there verbatim (useful for
// tests that need the unprocessed console output).
func ForwardConsoleLogs(r io.Reader, raw io.Writer) {
scanner := bufio.NewScanner(r)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ForwardConsoleLogs uses bufio.Scanner with the default 64K token limit. A single long console line (e.g., large JSON payloads or kernel messages) will trigger ErrTooLong, stop scanning, and drop all subsequent console output (only emitting a single WARN). Consider switching to a bufio.Reader ReadString/ReadBytes loop, or call scanner.Buffer with a larger max token size appropriate for expected console output.

Suggested change
scanner := bufio.NewScanner(r)
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

Copilot uses AI. Check for mistakes.
for scanner.Scan() {
line := scanner.Text()

if raw != nil {
raw.Write([]byte(line))
raw.Write([]byte("\n"))
}

if line == "" {
continue
}

if strings.HasPrefix(line, "{") {
if forwardJSONLog(line) {
continue
}
}

// Kernel message — parse optional "[ 1.234567] " timestamp prefix.
msg := line
attrs := []slog.Attr{slog.String("component", "kmsg")}
if after, ktime, ok := parseKernelTimestamp(line); ok {
msg = after
attrs = append(attrs, slog.String("ktime", ktime))
}
record := slog.NewRecord(time.Now(), slog.LevelInfo, msg, 0)
record.AddAttrs(attrs...)
handler := consoleHandler()
if handler.Enabled(context.Background(), slog.LevelInfo) {
handler.Handle(context.Background(), record) //nolint:errcheck
}
}
if err := scanner.Err(); err != nil {
record := slog.NewRecord(time.Now(), slog.LevelWarn, "console log reader stopped", 0)
record.AddAttrs(slog.String("component", "kmsg"), slog.Any("error", err))
handler := consoleHandler()
if handler.Enabled(context.Background(), slog.LevelWarn) {
handler.Handle(context.Background(), record) //nolint:errcheck
}
}
}
Comment on lines +124 to +166
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ForwardConsoleLogs uses bufio.Scanner but never checks scanner.Err() after the scan loop, so read errors (including token-too-long) will stop forwarding silently. Also, Scanner’s default max token size (64K) can truncate/abort on long kernel or JSON log lines. Consider increasing the scanner buffer (or using bufio.Reader) and handling scanner.Err() (at least logging it) to avoid dropping console logs unnoticed.

Copilot uses AI. Check for mistakes.

// forwardJSONLog attempts to parse line as a JSON slog record and emit it
// through the console handler. Returns true if the line was handled.
func forwardJSONLog(line string) bool {
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(line), &fields); err != nil {
return false
}

// A valid vminitd log must at least have "msg".
rawMsg, ok := fields["msg"]
if !ok {
return false
}

var msg string
if err := json.Unmarshal(rawMsg, &msg); err != nil {
return false
}
delete(fields, "msg")

var level slog.Level
if raw, ok := fields["level"]; ok {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
level.UnmarshalText([]byte(s)) //nolint:errcheck
}
delete(fields, "level")
}

// Discard the VM-side timestamp — the guest clock is not
// synchronised and typically reads as epoch.
delete(fields, "time")
t := time.Now()

handler := consoleHandler()
if !handler.Enabled(context.Background(), level) {
return true
}

record := slog.NewRecord(t, level, msg, 0)
for k, v := range fields {
var val any
if err := json.Unmarshal(v, &val); err == nil {
record.AddAttrs(slog.Any(k, val))
}
}

handler.Handle(context.Background(), record) //nolint:errcheck
return true
}

// parseKernelTimestamp extracts the "[ seconds.usecs] " prefix from a
// kernel log line. Returns the message after the prefix, the timestamp
// string, and whether a timestamp was found.
func parseKernelTimestamp(line string) (msg, ktime string, ok bool) {
if len(line) < 3 || line[0] != '[' {
return "", "", false
}
end := strings.IndexByte(line, ']')
if end < 0 {
return "", "", false
}
ktime = strings.TrimSpace(line[1:end])
msg = line[end+1:]
if len(msg) > 0 && msg[0] == ' ' {
msg = msg[1:]
}
return msg, ktime, true
}
Loading
Loading