diff --git a/cli/agent.go b/cli/agent.go index bf32a10c851cc..cb127ce292264 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -22,6 +22,8 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "cdr.dev/slog/sloggers/slogjson" + "cdr.dev/slog/sloggers/slogstackdriver" "github.com/coder/coder/agent" "github.com/coder/coder/agent/reaper" "github.com/coder/coder/buildinfo" @@ -32,14 +34,17 @@ import ( func (r *RootCmd) workspaceAgent() *clibase.Cmd { var ( - auth string - logDir string - pprofAddress string - noReap bool - sshMaxTimeout time.Duration - tailnetListenPort int64 - prometheusAddress string - debugAddress string + auth string + logDir string + pprofAddress string + noReap bool + sshMaxTimeout time.Duration + tailnetListenPort int64 + prometheusAddress string + debugAddress string + slogHumanPath string + slogJSONPath string + slogStackdriverPath string ) cmd := &clibase.Cmd{ Use: "agent", @@ -62,7 +67,46 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { MaxSize: 5, // MB } defer logWriter.Close() - logger := slog.Make(sloghuman.Sink(inv.Stderr), sloghuman.Sink(logWriter)).Leveled(slog.LevelDebug) + + sinks := []slog.Sink{sloghuman.Sink(logWriter)} + closers := []func() error{} + addSinkIfProvided := func(sinkFn func(io.Writer) slog.Sink, loc string) error { + switch loc { + case "": + + case "/dev/stdout": + sinks = append(sinks, sinkFn(inv.Stdout)) + + case "/dev/stderr": + sinks = append(sinks, sinkFn(inv.Stderr)) + + default: + fi, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) + if err != nil { + return xerrors.Errorf("open log file %q: %w", loc, err) + } + closers = append(closers, fi.Close) + sinks = append(sinks, sinkFn(fi)) + } + return nil + } + + if err := addSinkIfProvided(sloghuman.Sink, slogHumanPath); err != nil { + return xerrors.Errorf("add human sink: %w", err) + } + if err := addSinkIfProvided(slogjson.Sink, slogJSONPath); err != nil { + return xerrors.Errorf("add json sink: %w", err) + } + if err := addSinkIfProvided(slogstackdriver.Sink, slogStackdriverPath); err != nil { + return xerrors.Errorf("add stackdriver sink: %w", err) + } + + logger := slog.Make(sinks...).Leveled(slog.LevelDebug) + defer func() { + for _, closer := range closers { + _ = closer() + } + }() logger.Info(ctx, "spawning reaper process") // Do not start a reaper on the child process. It's important @@ -290,6 +334,30 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd { Value: clibase.StringOf(&debugAddress), Description: "The bind address to serve a debug HTTP server.", }, + { + Name: "Human Log Location", + Description: "Output human-readable logs to a given file.", + Flag: "log-human", + Env: "CODER_AGENT_LOGGING_HUMAN", + Default: "/dev/stderr", + Value: clibase.StringOf(&slogHumanPath), + }, + { + Name: "JSON Log Location", + Description: "Output JSON logs to a given file.", + Flag: "log-json", + Env: "CODER_AGENT_LOGGING_JSON", + Default: "", + Value: clibase.StringOf(&slogJSONPath), + }, + { + Name: "Stackdriver Log Location", + Description: "Output Stackdriver compatible logs to a given file.", + Flag: "log-stackdriver", + Env: "CODER_AGENT_LOGGING_STACKDRIVER", + Default: "", + Value: clibase.StringOf(&slogStackdriverPath), + }, } return cmd diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 5b9e6f394076e..90fff753cd365 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -3,6 +3,15 @@ Usage: coder agent [flags] Starts the Coder workspace agent. Options + --log-human string, $CODER_AGENT_LOGGING_HUMAN (default: /dev/stderr) + Output human-readable logs to a given file. + + --log-json string, $CODER_AGENT_LOGGING_JSON + Output JSON logs to a given file. + + --log-stackdriver string, $CODER_AGENT_LOGGING_STACKDRIVER + Output Stackdriver compatible logs to a given file. + --auth string, $CODER_AGENT_AUTH (default: token) Specify the authentication type to use for the agent.