diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go
new file mode 100644
index 0000000000000..8bc2799fa8b36
--- /dev/null
+++ b/cli/clilog/clilog.go
@@ -0,0 +1,211 @@
+package clilog
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "regexp"
+ "strings"
+
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+ "cdr.dev/slog/sloggers/sloghuman"
+ "cdr.dev/slog/sloggers/slogjson"
+ "cdr.dev/slog/sloggers/slogstackdriver"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+type (
+ Option func(*Builder)
+ Builder struct {
+ Filter []string
+ Human string
+ JSON string
+ Stackdriver string
+ Trace bool
+ Verbose bool
+ }
+)
+
+func New(opts ...Option) *Builder {
+ b := &Builder{}
+ for _, opt := range opts {
+ opt(b)
+ }
+ return b
+}
+
+func WithFilter(filters ...string) Option {
+ return func(b *Builder) {
+ b.Filter = filters
+ }
+}
+
+func WithHuman(loc string) Option {
+ return func(b *Builder) {
+ b.Human = loc
+ }
+}
+
+func WithJSON(loc string) Option {
+ return func(b *Builder) {
+ b.JSON = loc
+ }
+}
+
+func WithStackdriver(loc string) Option {
+ return func(b *Builder) {
+ b.Stackdriver = loc
+ }
+}
+
+func WithTrace() Option {
+ return func(b *Builder) {
+ b.Trace = true
+ }
+}
+
+func WithVerbose() Option {
+ return func(b *Builder) {
+ b.Verbose = true
+ }
+}
+
+func FromDeploymentValues(vals *codersdk.DeploymentValues) Option {
+ return func(b *Builder) {
+ b.Filter = vals.Logging.Filter.Value()
+ b.Human = vals.Logging.Human.Value()
+ b.JSON = vals.Logging.JSON.Value()
+ b.Stackdriver = vals.Logging.Stackdriver.Value()
+ b.Trace = vals.Trace.Enable.Value()
+ b.Verbose = vals.Verbose.Value()
+ }
+}
+
+func (b *Builder) Build(inv *clibase.Invocation) (log slog.Logger, closeLog func(), err error) {
+ var (
+ sinks = []slog.Sink{}
+ closers = []func() error{}
+ )
+ defer func() {
+ if err != nil {
+ for _, closer := range closers {
+ _ = closer()
+ }
+ }
+ }()
+
+ noopClose := func() {}
+
+ 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
+ }
+
+ err = addSinkIfProvided(sloghuman.Sink, b.Human)
+ if err != nil {
+ return slog.Logger{}, noopClose, xerrors.Errorf("add human sink: %w", err)
+ }
+ err = addSinkIfProvided(slogjson.Sink, b.JSON)
+ if err != nil {
+ return slog.Logger{}, noopClose, xerrors.Errorf("add json sink: %w", err)
+ }
+ err = addSinkIfProvided(slogstackdriver.Sink, b.Stackdriver)
+ if err != nil {
+ return slog.Logger{}, noopClose, xerrors.Errorf("add stackdriver sink: %w", err)
+ }
+
+ if b.Trace {
+ sinks = append(sinks, tracing.SlogSink{})
+ }
+
+ // User should log to null device if they don't want logs.
+ if len(sinks) == 0 {
+ return slog.Logger{}, noopClose, xerrors.New("no loggers provided, use /dev/null to disable logging")
+ }
+
+ filter := &debugFilterSink{next: sinks}
+
+ err = filter.compile(b.Filter)
+ if err != nil {
+ return slog.Logger{}, noopClose, xerrors.Errorf("compile filters: %w", err)
+ }
+
+ level := slog.LevelInfo
+ // Debug logging is always enabled if a filter is present.
+ if b.Verbose || filter.re != nil {
+ level = slog.LevelDebug
+ }
+
+ return inv.Logger.AppendSinks(filter).Leveled(level), func() {
+ for _, closer := range closers {
+ _ = closer()
+ }
+ }, nil
+}
+
+var _ slog.Sink = &debugFilterSink{}
+
+type debugFilterSink struct {
+ next []slog.Sink
+ re *regexp.Regexp
+}
+
+func (f *debugFilterSink) compile(res []string) error {
+ if len(res) == 0 {
+ return nil
+ }
+
+ var reb strings.Builder
+ for i, re := range res {
+ _, _ = fmt.Fprintf(&reb, "(%s)", re)
+ if i != len(res)-1 {
+ _, _ = reb.WriteRune('|')
+ }
+ }
+
+ re, err := regexp.Compile(reb.String())
+ if err != nil {
+ return xerrors.Errorf("compile regex: %w", err)
+ }
+ f.re = re
+ return nil
+}
+
+func (f *debugFilterSink) LogEntry(ctx context.Context, ent slog.SinkEntry) {
+ if ent.Level == slog.LevelDebug {
+ logName := strings.Join(ent.LoggerNames, ".")
+ if f.re != nil && !f.re.MatchString(logName) && !f.re.MatchString(ent.Message) {
+ return
+ }
+ }
+ for _, sink := range f.next {
+ sink.LogEntry(ctx, ent)
+ }
+}
+
+func (f *debugFilterSink) Sync() {
+ for _, sink := range f.next {
+ sink.Sync()
+ }
+}
diff --git a/cli/clilog/clilog_test.go b/cli/clilog/clilog_test.go
new file mode 100644
index 0000000000000..f7f854345043e
--- /dev/null
+++ b/cli/clilog/clilog_test.go
@@ -0,0 +1,243 @@
+package clilog_test
+
+import (
+ "encoding/json"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clilog"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBuilder(t *testing.T) {
+ t.Parallel()
+
+ t.Run("NoConfiguration", func(t *testing.T) {
+ t.Parallel()
+
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t),
+ }
+ err := cmd.Invoke().Run()
+ require.ErrorContains(t, err, "no loggers provided, use /dev/null to disable logging")
+ })
+
+ t.Run("Verbose", func(t *testing.T) {
+ t.Parallel()
+
+ tempFile := filepath.Join(t.TempDir(), "test.log")
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t,
+ clilog.WithHuman(tempFile),
+ clilog.WithVerbose(),
+ ),
+ }
+ err := cmd.Invoke().Run()
+ require.NoError(t, err)
+ assertLogs(t, tempFile, debugLog, infoLog, warnLog, filterLog)
+ })
+
+ t.Run("WithFilter", func(t *testing.T) {
+ t.Parallel()
+
+ tempFile := filepath.Join(t.TempDir(), "test.log")
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t,
+ clilog.WithHuman(tempFile),
+ // clilog.WithVerbose(), // implicit
+ clilog.WithFilter("important debug message"),
+ ),
+ }
+ err := cmd.Invoke().Run()
+ require.NoError(t, err)
+ assertLogs(t, tempFile, infoLog, warnLog, filterLog)
+ })
+
+ t.Run("WithHuman", func(t *testing.T) {
+ t.Parallel()
+
+ tempFile := filepath.Join(t.TempDir(), "test.log")
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t, clilog.WithHuman(tempFile)),
+ }
+ err := cmd.Invoke().Run()
+ require.NoError(t, err)
+ assertLogs(t, tempFile, infoLog, warnLog)
+ })
+
+ t.Run("WithJSON", func(t *testing.T) {
+ t.Parallel()
+
+ tempFile := filepath.Join(t.TempDir(), "test.log")
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t, clilog.WithJSON(tempFile), clilog.WithVerbose()),
+ }
+ err := cmd.Invoke().Run()
+ require.NoError(t, err)
+ assertLogsJSON(t, tempFile, debug, debugLog, info, infoLog, warn, warnLog, debug, filterLog)
+ })
+
+ t.Run("FromDeploymentValues", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Defaults", func(t *testing.T) {
+ stdoutPath := filepath.Join(t.TempDir(), "stdout")
+ stderrPath := filepath.Join(t.TempDir(), "stderr")
+
+ stdout, err := os.OpenFile(stdoutPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = stdout.Close() })
+
+ stderr, err := os.OpenFile(stderrPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = stderr.Close() })
+
+ // Use the default deployment values.
+ dv := coderdtest.DeploymentValues(t)
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t, clilog.FromDeploymentValues(dv)),
+ }
+ inv := cmd.Invoke()
+ inv.Stdout = stdout
+ inv.Stderr = stderr
+ err = inv.Run()
+ require.NoError(t, err)
+
+ assertLogs(t, stdoutPath, "")
+ assertLogs(t, stderrPath, infoLog, warnLog)
+ })
+
+ t.Run("Override", func(t *testing.T) {
+ tempFile := filepath.Join(t.TempDir(), "test.log")
+ tempJSON := filepath.Join(t.TempDir(), "test.json")
+ dv := &codersdk.DeploymentValues{
+ Logging: codersdk.LoggingConfig{
+ Filter: []string{"foo", "baz"},
+ Human: clibase.String(tempFile),
+ JSON: clibase.String(tempJSON),
+ },
+ Verbose: true,
+ Trace: codersdk.TraceConfig{
+ Enable: true,
+ },
+ }
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: testHandler(t, clilog.FromDeploymentValues(dv)),
+ }
+ err := cmd.Invoke().Run()
+ require.NoError(t, err)
+ assertLogs(t, tempFile, infoLog, warnLog)
+ assertLogsJSON(t, tempJSON, info, infoLog, warn, warnLog)
+ })
+ })
+
+ t.Run("NotFound", func(t *testing.T) {
+ t.Parallel()
+
+ tempFile := filepath.Join(t.TempDir(), "doesnotexist", "test.log")
+ cmd := &clibase.Cmd{
+ Use: "test",
+ Handler: func(inv *clibase.Invocation) error {
+ logger, closeLog, err := clilog.New(
+ clilog.WithFilter("foo", "baz"),
+ clilog.WithHuman(tempFile),
+ clilog.WithVerbose(),
+ ).Build(inv)
+ if err != nil {
+ return err
+ }
+ defer closeLog()
+ logger.Error(inv.Context(), "you will never see this")
+ return nil
+ },
+ }
+ err := cmd.Invoke().Run()
+ require.ErrorIs(t, err, fs.ErrNotExist)
+ })
+}
+
+var (
+ debug = "DEBUG"
+ info = "INFO"
+ warn = "WARN"
+ debugLog = "this is a debug message"
+ infoLog = "this is an info message"
+ warnLog = "this is a warning message"
+ filterLog = "this is an important debug message you want to see"
+)
+
+func testHandler(t testing.TB, opts ...clilog.Option) clibase.HandlerFunc {
+ t.Helper()
+
+ return func(inv *clibase.Invocation) error {
+ logger, closeLog, err := clilog.New(opts...).Build(inv)
+ if err != nil {
+ return err
+ }
+ defer closeLog()
+ logger.Debug(inv.Context(), debugLog)
+ logger.Info(inv.Context(), infoLog)
+ logger.Warn(inv.Context(), warnLog)
+ logger.Debug(inv.Context(), filterLog)
+ return nil
+ }
+}
+
+func assertLogs(t testing.TB, path string, expected ...string) {
+ t.Helper()
+
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+
+ logs := strings.Split(strings.TrimSpace(string(data)), "\n")
+ if !assert.Len(t, logs, len(expected)) {
+ t.Logf(string(data))
+ t.FailNow()
+ }
+ for i, log := range logs {
+ require.Contains(t, log, expected[i])
+ }
+}
+
+func assertLogsJSON(t testing.TB, path string, levelExpected ...string) {
+ t.Helper()
+
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+
+ if len(levelExpected)%2 != 0 {
+ t.Errorf("levelExpected must be a list of level-message pairs")
+ return
+ }
+
+ logs := strings.Split(strings.TrimSpace(string(data)), "\n")
+ if !assert.Len(t, logs, len(levelExpected)/2) {
+ t.Logf(string(data))
+ t.FailNow()
+ }
+ for i, log := range logs {
+ var entry struct {
+ Level string `json:"level"`
+ Message string `json:"msg"`
+ }
+ err := json.NewDecoder(strings.NewReader(log)).Decode(&entry)
+ require.NoError(t, err)
+ require.Equal(t, levelExpected[2*i], entry.Level)
+ require.Equal(t, levelExpected[2*i+1], entry.Message)
+ }
+}
diff --git a/cli/clilog/doc.go b/cli/clilog/doc.go
new file mode 100644
index 0000000000000..d32d68babe50a
--- /dev/null
+++ b/cli/clilog/doc.go
@@ -0,0 +1,2 @@
+// Package clilog provides a fluent API for configuring structured logging.
+package clilog
diff --git a/cli/server.go b/cli/server.go
index 2caf9f891286d..b4a4f0a654ef5 100644
--- a/cli/server.go
+++ b/cli/server.go
@@ -57,10 +57,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "cdr.dev/slog/sloggers/slogjson"
- "cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/cli/config"
@@ -325,7 +324,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
PrintLogo(inv, "Coder")
- logger, logCloser, err := BuildLogger(inv, vals)
+ logger, logCloser, err := clilog.New(clilog.FromDeploymentValues(vals)).Build(inv)
if err != nil {
return xerrors.Errorf("make logger: %w", err)
}
@@ -2011,121 +2010,6 @@ func IsLocalhost(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
-var _ slog.Sink = &debugFilterSink{}
-
-type debugFilterSink struct {
- next []slog.Sink
- re *regexp.Regexp
-}
-
-func (f *debugFilterSink) compile(res []string) error {
- if len(res) == 0 {
- return nil
- }
-
- var reb strings.Builder
- for i, re := range res {
- _, _ = fmt.Fprintf(&reb, "(%s)", re)
- if i != len(res)-1 {
- _, _ = reb.WriteRune('|')
- }
- }
-
- re, err := regexp.Compile(reb.String())
- if err != nil {
- return xerrors.Errorf("compile regex: %w", err)
- }
- f.re = re
- return nil
-}
-
-func (f *debugFilterSink) LogEntry(ctx context.Context, ent slog.SinkEntry) {
- if ent.Level == slog.LevelDebug {
- logName := strings.Join(ent.LoggerNames, ".")
- if f.re != nil && !f.re.MatchString(logName) && !f.re.MatchString(ent.Message) {
- return
- }
- }
- for _, sink := range f.next {
- sink.LogEntry(ctx, ent)
- }
-}
-
-func (f *debugFilterSink) Sync() {
- for _, sink := range f.next {
- sink.Sync()
- }
-}
-
-func BuildLogger(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (slog.Logger, func(), error) {
- var (
- sinks = []slog.Sink{}
- 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
- }
-
- err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.String())
- if err != nil {
- return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err)
- }
- err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.String())
- if err != nil {
- return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err)
- }
- err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.String())
- if err != nil {
- return slog.Logger{}, nil, xerrors.Errorf("add stackdriver sink: %w", err)
- }
-
- if cfg.Trace.CaptureLogs {
- sinks = append(sinks, tracing.SlogSink{})
- }
-
- // User should log to null device if they don't want logs.
- if len(sinks) == 0 {
- return slog.Logger{}, nil, xerrors.New("no loggers provided")
- }
-
- filter := &debugFilterSink{next: sinks}
-
- err = filter.compile(cfg.Logging.Filter.Value())
- if err != nil {
- return slog.Logger{}, nil, xerrors.Errorf("compile filters: %w", err)
- }
-
- level := slog.LevelInfo
- // Debug logging is always enabled if a filter is present.
- if cfg.Verbose || filter.re != nil {
- level = slog.LevelDebug
- }
-
- return inv.Logger.AppendSinks(filter).Leveled(level), func() {
- for _, closer := range closers {
- _ = closer()
- }
- }, nil
-}
-
func ConnectToPostgres(ctx context.Context, logger slog.Logger, driver string, dbURL string) (sqlDB *sql.DB, err error) {
logger.Debug(ctx, "connecting to postgresql")
diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md
index e28a8adafeb05..643707f621ae8 100644
--- a/docs/cli/provisionerd_start.md
+++ b/docs/cli/provisionerd_start.md
@@ -22,6 +22,43 @@ coder provisionerd start [flags]
Directory to store cached data.
+### --log-filter
+
+| | |
+| ----------- | ------------------------------------------------- |
+| Type | string-array
|
+| Environment | $CODER_PROVISIONER_DAEMON_LOG_FILTER
|
+
+Filter debug logs by matching against a given regex. Use .\* to match all debug logs.
+
+### --log-human
+
+| | |
+| ----------- | ---------------------------------------------------- |
+| Type | string
|
+| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_HUMAN
|
+| Default | /dev/stderr
|
+
+Output human-readable logs to a given file.
+
+### --log-json
+
+| | |
+| ----------- | --------------------------------------------------- |
+| Type | string
|
+| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_JSON
|
+
+Output JSON logs to a given file.
+
+### --log-stackdriver
+
+| | |
+| ----------- | ---------------------------------------------------------- |
+| Type | string
|
+| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER
|
+
+Output Stackdriver compatible logs to a given file.
+
### --name
| | |
@@ -68,3 +105,13 @@ Pre-shared key to authenticate with Coder server.
| Environment | $CODER_PROVISIONERD_TAGS
|
Tags to filter provisioner jobs by.
+
+### --verbose
+
+| | |
+| ----------- | ---------------------------------------------- |
+| Type | bool
|
+| Environment | $CODER_PROVISIONER_DAEMON_VERBOSE
|
+| Default | false
|
+
+Output debug-level logs.
diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go
index 7200464730841..576f4f5e8b602 100644
--- a/enterprise/cli/provisionerdaemons.go
+++ b/enterprise/cli/provisionerdaemons.go
@@ -16,6 +16,7 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
agpl "github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/coderd/database"
@@ -55,12 +56,17 @@ func validateProvisionerDaemonName(name string) error {
func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
var (
- cacheDir string
- rawTags []string
- pollInterval time.Duration
- pollJitter time.Duration
- preSharedKey string
- name string
+ cacheDir string
+ logHuman string
+ logJSON string
+ logStackdriver string
+ logFilter []string
+ name string
+ rawTags []string
+ pollInterval time.Duration
+ pollJitter time.Duration
+ preSharedKey string
+ verbose bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -89,9 +95,23 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
return err
}
- logger := slog.Make(sloghuman.Sink(inv.Stderr))
- if ok, _ := inv.ParsedFlags().GetBool("verbose"); ok {
- logger = logger.Leveled(slog.LevelDebug)
+ logOpts := []clilog.Option{
+ clilog.WithFilter(logFilter...),
+ clilog.WithHuman(logHuman),
+ clilog.WithJSON(logJSON),
+ clilog.WithStackdriver(logStackdriver),
+ }
+ if verbose {
+ logOpts = append(logOpts, clilog.WithVerbose())
+ }
+
+ logger, closeLogger, err := clilog.New(logOpts...).Build(inv)
+ if err != nil {
+ // Fall back to a basic logger
+ logger = slog.Make(sloghuman.Sink(inv.Stderr))
+ logger.Error(ctx, "failed to initialize logger", slog.Error(err))
+ } else {
+ defer closeLogger()
}
if len(tags) != 0 {
@@ -234,6 +254,41 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
Value: clibase.StringOf(&name),
Default: "",
},
+ {
+ Flag: "verbose",
+ Env: "CODER_PROVISIONER_DAEMON_VERBOSE",
+ Description: "Output debug-level logs.",
+ Value: clibase.BoolOf(&verbose),
+ Default: "false",
+ },
+ {
+ Flag: "log-human",
+ Env: "CODER_PROVISIONER_DAEMON_LOGGING_HUMAN",
+ Description: "Output human-readable logs to a given file.",
+ Value: clibase.StringOf(&logHuman),
+ Default: "/dev/stderr",
+ },
+ {
+ Flag: "log-json",
+ Env: "CODER_PROVISIONER_DAEMON_LOGGING_JSON",
+ Description: "Output JSON logs to a given file.",
+ Value: clibase.StringOf(&logJSON),
+ Default: "",
+ },
+ {
+ Flag: "log-stackdriver",
+ Env: "CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER",
+ Description: "Output Stackdriver compatible logs to a given file.",
+ Value: clibase.StringOf(&logStackdriver),
+ Default: "",
+ },
+ {
+ Flag: "log-filter",
+ Env: "CODER_PROVISIONER_DAEMON_LOG_FILTER",
+ Description: "Filter debug logs by matching against a given regex. Use .* to match all debug logs.",
+ Value: clibase.StringArrayOf(&logFilter),
+ Default: "",
+ },
}
return cmd
diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go
index bf5db6d0f7fb2..4e37077b3c90f 100644
--- a/enterprise/cli/proxyserver.go
+++ b/enterprise/cli/proxyserver.go
@@ -23,6 +23,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/httpapi"
@@ -121,7 +122,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd {
go cli.DumpHandler(ctx)
cli.PrintLogo(inv, "Coder Workspace Proxy")
- logger, logCloser, err := cli.BuildLogger(inv, cfg)
+ logger, logCloser, err := clilog.New(clilog.FromDeploymentValues(cfg)).Build(inv)
if err != nil {
return xerrors.Errorf("make logger: %w", err)
}
diff --git a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
index b497c5ab6231c..042e788c12c46 100644
--- a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
+++ b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
@@ -9,6 +9,19 @@ OPTIONS:
-c, --cache-dir string, $CODER_CACHE_DIRECTORY (default: [cache dir])
Directory to store cached data.
+ --log-filter string-array, $CODER_PROVISIONER_DAEMON_LOG_FILTER
+ Filter debug logs by matching against a given regex. Use .* to match
+ all debug logs.
+
+ --log-human string, $CODER_PROVISIONER_DAEMON_LOGGING_HUMAN (default: /dev/stderr)
+ Output human-readable logs to a given file.
+
+ --log-json string, $CODER_PROVISIONER_DAEMON_LOGGING_JSON
+ Output JSON logs to a given file.
+
+ --log-stackdriver string, $CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER
+ Output Stackdriver compatible logs to a given file.
+
--name string, $CODER_PROVISIONER_DAEMON_NAME
Name of this provisioner daemon. Defaults to the current hostname
without FQDN.
@@ -25,5 +38,8 @@ OPTIONS:
-t, --tag string-array, $CODER_PROVISIONERD_TAGS
Tags to filter provisioner jobs by.
+ --verbose bool, $CODER_PROVISIONER_DAEMON_VERBOSE (default: false)
+ Output debug-level logs.
+
———
Run `coder --help` for a list of global options.