Add structured daemon lifecycle logs
VM start, image build, and network/setup failures were hard to diagnose because bangerd emitted almost no lifecycle logs and the Firecracker SDK logger was discarded. This adds a daemon-wide JSON logger with configurable log level so failures leave breadcrumbs instead of only side effects. Log the main daemon and VM lifecycle stages, preserve raw Firecracker and image-build helper output in dedicated files, and include those log paths in daemon status and returned errors. Bridge SDK logrus output into the daemon logger at debug level so low-level Firecracker diagnostics are available without making normal info logs unreadable. Validation: go test ./... and make build. Left unrelated worktree changes out of this commit, including internal/api/types.go, the deleted shell scripts, and my-rootfs.ext4.
This commit is contained in:
parent
5018bc6170
commit
644e60d739
13 changed files with 746 additions and 31 deletions
|
|
@ -3,6 +3,7 @@ package firecracker
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
|
@ -27,6 +28,7 @@ type MachineConfig struct {
|
|||
TapDevice string
|
||||
VCPUCount int
|
||||
MemoryMiB int
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
|
|
@ -50,7 +52,7 @@ func NewMachine(ctx context.Context, cfg MachineConfig) (*Machine, error) {
|
|||
ctx,
|
||||
buildConfig(cfg),
|
||||
sdk.WithProcessRunner(cmd),
|
||||
sdk.WithLogger(newLogger()),
|
||||
sdk.WithLogger(newLogger(cfg.Logger)),
|
||||
)
|
||||
if err != nil {
|
||||
if logFile != nil {
|
||||
|
|
@ -80,8 +82,8 @@ func (m *Machine) PID() (int, error) {
|
|||
return m.machine.PID()
|
||||
}
|
||||
|
||||
func New(apiSock string) *Client {
|
||||
return &Client{client: sdk.NewClient(apiSock, newLogger(), false)}
|
||||
func New(apiSock string, logger *slog.Logger) *Client {
|
||||
return &Client{client: sdk.NewClient(apiSock, newLogger(logger), false)}
|
||||
}
|
||||
|
||||
func (c *Client) SendCtrlAltDel(ctx context.Context) error {
|
||||
|
|
@ -150,12 +152,44 @@ func shellQuote(value string) string {
|
|||
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
||||
func newLogger() *logrus.Entry {
|
||||
func newLogger(base *slog.Logger) *logrus.Entry {
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
logger.AddHook(slogHook{logger: base})
|
||||
return logrus.NewEntry(logger)
|
||||
}
|
||||
|
||||
type slogHook struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (h slogHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func (h slogHook) Fire(entry *logrus.Entry) error {
|
||||
if h.logger == nil {
|
||||
return nil
|
||||
}
|
||||
level := slog.LevelDebug
|
||||
switch entry.Level {
|
||||
case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel:
|
||||
level = slog.LevelError
|
||||
case logrus.WarnLevel:
|
||||
level = slog.LevelWarn
|
||||
default:
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
attrs := make([]any, 0, len(entry.Data)*2+2)
|
||||
attrs = append(attrs, "component", "firecracker_sdk")
|
||||
for key, value := range entry.Data {
|
||||
attrs = append(attrs, key, value)
|
||||
}
|
||||
h.logger.Log(context.Background(), level, entry.Message, attrs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Machine) closeLog() {
|
||||
m.closeOnce.Do(func() {
|
||||
if m.logFile != nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package firecracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -76,3 +79,34 @@ func TestBuildProcessRunnerUsesSudoWrapper(t *testing.T) {
|
|||
t.Fatalf("script = %q, want %q", cmd.Args[4], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKLoggerBridgeEmitsStructuredDebugLogs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
|
||||
entry := newLogger(logger)
|
||||
entry.WithField("vm_id", "vm-1").Info("sdk ready")
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, `"component":"firecracker_sdk"`) {
|
||||
t.Fatalf("output = %q, want firecracker_sdk component", output)
|
||||
}
|
||||
if !strings.Contains(output, `"vm_id":"vm-1"`) {
|
||||
t.Fatalf("output = %q, want vm_id field", output)
|
||||
}
|
||||
if !strings.Contains(output, `"msg":"sdk ready"`) {
|
||||
t.Fatalf("output = %q, want sdk message", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKLoggerBridgeSuppressesDebugAtInfoLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
entry := newLogger(logger)
|
||||
entry.Info("sdk hidden at info")
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Fatalf("expected info-level logger to suppress sdk debug chatter, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue