Skip to content

Commit 1bc2a30

Browse files
authored
internal/nix: log all command starts and exits (#2135)
Write logs whenever a Nix command is about to start and after it exits. Make `cmd` a `slog.LogValuer` so that it writes structured log attributes grouped under a `cmd` prefix. Example logs and attributes (line breaks added for readability): msg="nix command starting" cmd.args="nix --extra-experimental-features ca-derivations --option experimental-features 'nix-command flakes fetch-closure' eval --impure --raw --expr builtins.currentSystem" cmd.path=/nix/var/nix/profiles/default/bin/nix msg="nix command exited" cmd.args="nix --extra-experimental-features ca-derivations --option experimental-features 'nix-command flakes fetch-closure' eval --impure --raw --expr builtins.currentSystem" cmd.path=/nix/var/nix/profiles/default/bin/nix cmd.pid=68672 cmd.code=0 cmd.dur=34.296041ms Command exits are always logged at the debug level, even if the command exits with a non-zero exit code. This is because some commands might be expected to fail (for example, when checking if a path exists in the store).
1 parent 10b7bcc commit 1bc2a30

File tree

5 files changed

+120
-57
lines changed

5 files changed

+120
-57
lines changed

internal/nix/build.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,5 @@ func Build(ctx context.Context, args *BuildArgs, installables ...string) error {
4444
cmd.Stdin = os.Stdin
4545
cmd.Stdout = args.Writer
4646
cmd.Stderr = args.Writer
47-
48-
slog.Debug("running cmd", "cmd", cmd)
4947
return cmd.Run(ctx)
5048
}

internal/nix/command.go

Lines changed: 120 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"log/slog"
910
"os"
1011
"os/exec"
1112
"slices"
1213
"strconv"
1314
"strings"
14-
"sync"
1515
"syscall"
1616
"time"
1717
)
@@ -24,8 +24,10 @@ type cmd struct {
2424
Stdout io.Writer
2525
Stderr io.Writer
2626

27-
execCmd *exec.Cmd
28-
execCmdOnce sync.Once
27+
execCmd *exec.Cmd
28+
err error
29+
dur time.Duration
30+
logger *slog.Logger
2931
}
3032

3133
func command(args ...any) *cmd {
@@ -35,69 +37,141 @@ func command(args ...any) *cmd {
3537
"--extra-experimental-features", "ca-derivations",
3638
"--option", "experimental-features", "nix-command flakes fetch-closure",
3739
}, args...),
40+
logger: slog.Default(),
3841
}
3942
return cmd
4043
}
4144

4245
func (c *cmd) CombinedOutput(ctx context.Context) ([]byte, error) {
43-
out, err := c.initExecCommand(ctx).CombinedOutput()
44-
return out, c.error(ctx, err)
46+
cmd := c.initExecCommand(ctx)
47+
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
48+
49+
start := time.Now()
50+
out, err := cmd.CombinedOutput()
51+
c.dur = time.Since(start)
52+
53+
c.err = c.error(ctx, err)
54+
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
55+
return out, c.err
4556
}
4657

4758
func (c *cmd) Output(ctx context.Context) ([]byte, error) {
48-
out, err := c.initExecCommand(ctx).Output()
49-
return out, c.error(ctx, err)
59+
cmd := c.initExecCommand(ctx)
60+
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
61+
62+
start := time.Now()
63+
out, err := cmd.Output()
64+
c.dur = time.Since(start)
65+
66+
c.err = c.error(ctx, err)
67+
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
68+
return out, c.err
5069
}
5170

5271
func (c *cmd) Run(ctx context.Context) error {
53-
return c.error(ctx, c.initExecCommand(ctx).Run())
72+
cmd := c.initExecCommand(ctx)
73+
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
74+
75+
start := time.Now()
76+
err := cmd.Run()
77+
c.dur = time.Since(start)
78+
79+
c.err = c.error(ctx, err)
80+
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
81+
return c.err
82+
}
83+
84+
func (c *cmd) LogValue() slog.Value {
85+
attrs := []slog.Attr{
86+
slog.Any("args", c.Args),
87+
}
88+
if c.execCmd == nil {
89+
return slog.GroupValue(attrs...)
90+
}
91+
attrs = append(attrs, slog.String("path", c.execCmd.Path))
92+
93+
var exitErr *exec.ExitError
94+
if errors.As(c.err, &exitErr) {
95+
stderr := c.stderrExcerpt(exitErr.Stderr)
96+
if len(stderr) != 0 {
97+
attrs = append(attrs, slog.String("stderr", stderr))
98+
}
99+
}
100+
if proc := c.execCmd.Process; proc != nil {
101+
attrs = append(attrs, slog.Int("pid", proc.Pid))
102+
}
103+
if procState := c.execCmd.ProcessState; procState != nil {
104+
if procState.Exited() {
105+
attrs = append(attrs, slog.Int("code", procState.ExitCode()))
106+
}
107+
if status, ok := procState.Sys().(syscall.WaitStatus); ok && status.Signaled() {
108+
if status.Signaled() {
109+
attrs = append(attrs, slog.String("signal", status.Signal().String()))
110+
}
111+
}
112+
}
113+
if c.dur != 0 {
114+
attrs = append(attrs, slog.Duration("dur", c.dur))
115+
}
116+
return slog.GroupValue(attrs...)
54117
}
55118

56119
func (c *cmd) String() string {
57120
return c.Args.String()
58121
}
59122

60123
func (c *cmd) initExecCommand(ctx context.Context) *exec.Cmd {
61-
c.execCmdOnce.Do(func() {
62-
args := c.Args.StringSlice()
63-
c.execCmd = exec.CommandContext(ctx, args[0], args[1:]...)
64-
c.execCmd.Env = c.Env
65-
c.execCmd.Stdin = c.Stdin
66-
c.execCmd.Stdout = c.Stdout
67-
c.execCmd.Stderr = c.Stderr
68-
69-
c.execCmd.Cancel = func() error {
70-
// Try to let Nix exit gracefully by sending an
71-
// interrupt instead of the default behavior of killing
72-
// it.
73-
err := c.execCmd.Process.Signal(os.Interrupt)
74-
if errors.Is(err, os.ErrProcessDone) {
75-
// Nix already exited; execCmd.Wait will use the
76-
// exit code.
77-
return err
78-
}
79-
if err != nil {
80-
// We failed to send SIGINT, so kill the process
81-
// instead.
82-
//
83-
// - If Nix already exited, Kill will return
84-
// os.ErrProcessDone and execCmd.Wait will use
85-
// the exit code.
86-
// - Otherwise, execCmd.Wait will always return
87-
// an error.
88-
return c.execCmd.Process.Kill()
89-
}
124+
if c.execCmd != nil {
125+
return c.execCmd
126+
}
90127

91-
// We sent the SIGINT successfully. It's still possible
92-
// for Nix to exit successfully, so return
93-
// os.ErrProcessDone so that execCmd.Wait uses the exit
94-
// code instead of ctx.Err.
95-
return os.ErrProcessDone
128+
args := c.Args.StringSlice()
129+
c.execCmd = exec.CommandContext(ctx, args[0], args[1:]...)
130+
c.execCmd.Env = c.Env
131+
c.execCmd.Stdin = c.Stdin
132+
c.execCmd.Stdout = c.Stdout
133+
c.execCmd.Stderr = c.Stderr
134+
135+
c.execCmd.Cancel = func() error {
136+
// Try to let Nix exit gracefully by sending an interrupt
137+
// instead of the default behavior of killing it.
138+
c.logger.DebugContext(ctx, "sending interrupt to nix process", slog.Group("cmd",
139+
"args", c.Args,
140+
"path", c.execCmd.Path,
141+
"pid", c.execCmd.Process.Pid,
142+
))
143+
err := c.execCmd.Process.Signal(os.Interrupt)
144+
if errors.Is(err, os.ErrProcessDone) {
145+
// Nix already exited; execCmd.Wait will use the exit
146+
// code.
147+
return err
96148
}
97-
// Kill Nix if it doesn't exit within 15 seconds of Devbox
98-
// sending an interrupt.
99-
c.execCmd.WaitDelay = 15 * time.Second
100-
})
149+
if err != nil {
150+
// We failed to send SIGINT, so kill the process
151+
// instead.
152+
//
153+
// - If Nix already exited, Kill will return
154+
// os.ErrProcessDone and execCmd.Wait will use
155+
// the exit code.
156+
// - Otherwise, execCmd.Wait will always return an
157+
// error.
158+
c.logger.ErrorContext(ctx, "error interrupting nix process, attempting to kill",
159+
"err", err, slog.Group("cmd",
160+
"args", c.Args,
161+
"path", c.execCmd.Path,
162+
"pid", c.execCmd.Process.Pid,
163+
))
164+
return c.execCmd.Process.Kill()
165+
}
166+
167+
// We sent the SIGINT successfully. It's still possible for Nix
168+
// to exit successfully, so return os.ErrProcessDone so that
169+
// execCmd.Wait uses the exit code instead of ctx.Err.
170+
return os.ErrProcessDone
171+
}
172+
// Kill Nix if it doesn't exit within 15 seconds of Devbox sending an
173+
// interrupt.
174+
c.execCmd.WaitDelay = 15 * time.Second
101175
return c.execCmd
102176
}
103177

internal/nix/profiles.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"fmt"
1111
"io"
1212
"io/fs"
13-
"log/slog"
1413
"os"
1514
"path/filepath"
1615

@@ -60,14 +59,10 @@ func ProfileInstall(ctx context.Context, args *ProfileInstallArgs) error {
6059
// However, now we do the building in nix.Build, by the time we install in profile everything
6160
// should already be in the store. We need to capture the output so we can decide if a conflict
6261
// happened.
63-
64-
slog.Debug("running command", "cmd", cmd)
6562
out, err := cmd.CombinedOutput(ctx)
66-
6763
if bytes.Contains(out, []byte("error: An existing package already provides the following file")) {
6864
return ErrPriorityConflict
6965
}
70-
7166
return err
7267
}
7368

internal/nix/search.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"log/slog"
87
"os"
98
"regexp"
109
"strings"
@@ -103,7 +102,6 @@ func searchSystem(url, system string) (map[string]*Info, error) {
103102
if system != "" {
104103
cmd.Args = append(cmd.Args, "--system", system)
105104
}
106-
slog.Debug("running command", "cmd", cmd)
107105
out, err := cmd.Output(context.TODO())
108106
if err != nil {
109107
// for now, assume all errors are invalid packages.

internal/nix/store.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ func StorePathsFromInstallable(ctx context.Context, installable string, allowIns
3535
cmd.Env = allowInsecureEnv(cmd.Env)
3636
}
3737

38-
slog.Debug("running cmd", "cmd", cmd)
3938
resultBytes, err := cmd.Output(ctx)
4039
if err != nil {
4140
return nil, err
@@ -57,7 +56,6 @@ func StorePathsAreInStore(ctx context.Context, storePaths []string) (map[string]
5756
}
5857
cmd := command("path-info", "--offline", "--json")
5958
cmd.Args = appendArgs(cmd.Args, storePaths)
60-
slog.Debug("running cmd", "cmd", cmd)
6159
output, err := cmd.Output(ctx)
6260
if err != nil {
6361
return nil, err

0 commit comments

Comments
 (0)