From 1377d010bf1581899a110c343760a81665a8f2ca Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 11:23:23 +0000 Subject: [PATCH 1/9] refactor(coder/cli): extract BuildLogger to clilog --- cli/clilog/clilog.go | 192 ++++++++++++++++++++++++++++++++++++++ cli/clilog/clilog_test.go | 168 +++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 cli/clilog/clilog.go create mode 100644 cli/clilog/clilog_test.go diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go new file mode 100644 index 0000000000000..50989aeacf53c --- /dev/null +++ b/cli/clilog/clilog.go @@ -0,0 +1,192 @@ +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) +type Builder struct { + Filter []string + Human string + JSON string + Stackdriver string + Trace bool + Verbose bool +} + +func New() *Builder { + return &Builder{ + Human: "/dev/stderr", + Filter: []string{}, + } +} + +func (b *Builder) WithFilter(filters ...string) *Builder { + b.Filter = filters + return b +} + +func (b *Builder) WithHuman(loc string) *Builder { + b.Human = loc + return b +} + +func (b *Builder) WithJSON(loc string) *Builder { + b.JSON = loc + return b +} + +func (b *Builder) WithStackdriver(loc string) *Builder { + b.Stackdriver = loc + return b +} + +func (b *Builder) WithTrace() *Builder { + b.Trace = true + return b + +} +func (b *Builder) WithVerbose() *Builder { + b.Verbose = true + return b +} + +func (b *Builder) FromDeploymentValues(vals *codersdk.DeploymentValues) *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() + return b +} + +func (b *Builder) Build(inv *clibase.Invocation) (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, b.Human) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err) + } + err = addSinkIfProvided(slogjson.Sink, b.JSON) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err) + } + err = addSinkIfProvided(slogstackdriver.Sink, b.Stackdriver) + if err != nil { + return slog.Logger{}, nil, 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{}, nil, xerrors.New("no loggers provided") + } + + filter := &debugFilterSink{next: sinks} + + err = filter.compile(b.Filter) + 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 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..357b055a3f384 --- /dev/null +++ b/cli/clilog/clilog_test.go @@ -0,0 +1,168 @@ +package clilog_test + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/coder/coder/v2/cli/clibase" + "github.com/coder/coder/v2/cli/clilog" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuilder(t *testing.T) { + t.Parallel() + + t.Run("WithFilter", func(t *testing.T) { + t.Parallel() + + tempFile := filepath.Join(t.TempDir(), "test.log") + cmd := &clibase.Cmd{ + Use: "test", + Handler: func(inv *clibase.Invocation) error { + logger, closeLog, err := clilog.New(). + WithFilter("foo", "baz"). + WithHuman(tempFile). + WithVerbose(). + Build(inv) + if err != nil { + return err + } + defer closeLog() + logger.Debug(inv.Context(), "foo is not a useful message") + logger.Debug(inv.Context(), "bar is also not a useful message") + return nil + }, + } + err := cmd.Invoke().Run() + require.NoError(t, err) + + data, err := os.ReadFile(tempFile) + require.NoError(t, err) + logs := strings.Split(strings.TrimSpace(string(data)), "\n") + if !assert.Len(t, logs, 1) { + t.Logf(string(data)) + t.FailNow() + } + require.Contains(t, logs[0], "foo is not a useful message") + }) + + t.Run("WithHuman", func(t *testing.T) { + t.Parallel() + + tempFile := filepath.Join(t.TempDir(), "test.log") + cmd := &clibase.Cmd{ + Use: "test", + Handler: func(inv *clibase.Invocation) error { + logger, closeLog, err := clilog.New(). + WithHuman(tempFile). + Build(inv) + if err != nil { + return err + } + defer closeLog() + logger.Debug(inv.Context(), "foo is not a useful message") + logger.Info(inv.Context(), "bar is also not a useful message") + return nil + }, + } + err := cmd.Invoke().Run() + require.NoError(t, err) + + data, err := os.ReadFile(tempFile) + require.NoError(t, err) + logs := strings.Split(strings.TrimSpace(string(data)), "\n") + if !assert.Len(t, logs, 1) { + t.Logf(string(data)) + t.FailNow() + } + require.Contains(t, logs[0], "bar is also not a useful message") + }) + + t.Run("WithJSON", func(t *testing.T) { + t.Parallel() + + tempFile := filepath.Join(t.TempDir(), "test.log") + cmd := &clibase.Cmd{ + Use: "test", + Handler: func(inv *clibase.Invocation) error { + logger, closeLog, err := clilog.New(). + WithJSON(tempFile). + Build(inv) + if err != nil { + return err + } + defer closeLog() + logger.Debug(inv.Context(), "foo is not a useful message") + logger.Info(inv.Context(), "bar is also not a useful message") + return nil + }, + } + err := cmd.Invoke().Run() + require.NoError(t, err) + + data, err := os.ReadFile(tempFile) + require.NoError(t, err) + logs := strings.Split(strings.TrimSpace(string(data)), "\n") + if !assert.Len(t, logs, 1) { + t.Logf(string(data)) + t.FailNow() + } + require.Contains(t, logs[0], "bar") + var entry struct { + Level string `json:"level"` + Message string `json:"msg"` + } + + err = json.NewDecoder(strings.NewReader(logs[0])).Decode(&entry) + require.NoError(t, err) + require.Equal(t, "INFO", entry.Level) + require.Equal(t, "bar is also not a useful message", entry.Message) + }) + + t.Run("WithStackdriver", func(t *testing.T) { + t.Parallel() + + tempFile := filepath.Join(t.TempDir(), "test.log") + cmd := &clibase.Cmd{ + Use: "test", + Handler: func(inv *clibase.Invocation) error { + logger, closeLog, err := clilog.New(). + WithStackdriver(tempFile). + Build(inv) + if err != nil { + return err + } + defer closeLog() + logger.Debug(inv.Context(), "foo is not a useful message") + logger.Info(inv.Context(), "bar is also not a useful message") + return nil + }, + } + err := cmd.Invoke().Run() + require.NoError(t, err) + + data, err := os.ReadFile(tempFile) + require.NoError(t, err) + logs := strings.Split(strings.TrimSpace(string(data)), "\n") + if !assert.Len(t, logs, 1) { + t.Logf(string(data)) + t.FailNow() + } + require.Contains(t, logs[0], "bar is also not a useful message") + + var entry struct { + Severity string `json:"severity"` + Message string `json:"message"` + } + + err = json.NewDecoder(strings.NewReader(logs[0])).Decode(&entry) + require.NoError(t, err) + require.Equal(t, "INFO", entry.Severity) + require.Equal(t, "bar is also not a useful message", entry.Message) + }) +} From 1963c14a9f91d4fa0b61d0f53f10f61cacc54d71 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 11:46:35 +0000 Subject: [PATCH 2/9] move to functional options --- cli/clilog/clilog.go | 67 +++++++++++++++++++++++---------------- cli/clilog/clilog_test.go | 57 ++++++++++++++++++++------------- cli/clilog/doc.go | 2 ++ 3 files changed, 76 insertions(+), 50 deletions(-) create mode 100644 cli/clilog/doc.go diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go index 50989aeacf53c..682dd5a4021e1 100644 --- a/cli/clilog/clilog.go +++ b/cli/clilog/clilog.go @@ -29,51 +29,62 @@ type Builder struct { Verbose bool } -func New() *Builder { - return &Builder{ +func New(opts ...Option) *Builder { + b := &Builder{ Human: "/dev/stderr", Filter: []string{}, } + for _, opt := range opts { + opt(b) + } + return b } -func (b *Builder) WithFilter(filters ...string) *Builder { - b.Filter = filters - return b +func WithFilter(filters ...string) Option { + return func(b *Builder) { + b.Filter = filters + } } -func (b *Builder) WithHuman(loc string) *Builder { - b.Human = loc - return b +func WithHuman(loc string) Option { + return func(b *Builder) { + b.Human = loc + } } -func (b *Builder) WithJSON(loc string) *Builder { - b.JSON = loc - return b +func WithJSON(loc string) Option { + return func(b *Builder) { + b.JSON = loc + } } -func (b *Builder) WithStackdriver(loc string) *Builder { - b.Stackdriver = loc - return b +func WithStackdriver(loc string) Option { + return func(b *Builder) { + b.Stackdriver = loc + } } -func (b *Builder) WithTrace() *Builder { - b.Trace = true - return b +func WithTrace() Option { + return func(b *Builder) { + b.Trace = true + } } -func (b *Builder) WithVerbose() *Builder { - b.Verbose = true - return b +func WithVerbose() Option { + return func(b *Builder) { + b.Verbose = true + } } -func (b *Builder) FromDeploymentValues(vals *codersdk.DeploymentValues) *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() - return b +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) (slog.Logger, func(), error) { diff --git a/cli/clilog/clilog_test.go b/cli/clilog/clilog_test.go index 357b055a3f384..321ac055b5372 100644 --- a/cli/clilog/clilog_test.go +++ b/cli/clilog/clilog_test.go @@ -9,6 +9,7 @@ import ( "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/clilog" + "github.com/coder/coder/v2/codersdk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,11 +25,11 @@ func TestBuilder(t *testing.T) { cmd := &clibase.Cmd{ Use: "test", Handler: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New(). - WithFilter("foo", "baz"). - WithHuman(tempFile). - WithVerbose(). - Build(inv) + logger, closeLog, err := clilog.New( + clilog.WithFilter("foo", "baz"), + clilog.WithHuman(tempFile), + clilog.WithVerbose(), + ).Build(inv) if err != nil { return err } @@ -58,8 +59,8 @@ func TestBuilder(t *testing.T) { cmd := &clibase.Cmd{ Use: "test", Handler: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New(). - WithHuman(tempFile). + logger, closeLog, err := clilog.New( + clilog.WithHuman(tempFile)). Build(inv) if err != nil { return err @@ -90,8 +91,8 @@ func TestBuilder(t *testing.T) { cmd := &clibase.Cmd{ Use: "test", Handler: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New(). - WithJSON(tempFile). + logger, closeLog, err := clilog.New( + clilog.WithJSON(tempFile)). Build(inv) if err != nil { return err @@ -124,15 +125,26 @@ func TestBuilder(t *testing.T) { require.Equal(t, "bar is also not a useful message", entry.Message) }) - t.Run("WithStackdriver", func(t *testing.T) { + t.Run("FromDeploymentValues", func(t *testing.T) { t.Parallel() 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: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New(). - WithStackdriver(tempFile). + logger, closeLog, err := clilog.New(clilog.FromDeploymentValues(dv)). Build(inv) if err != nil { return err @@ -149,20 +161,21 @@ func TestBuilder(t *testing.T) { data, err := os.ReadFile(tempFile) require.NoError(t, err) logs := strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 1) { + if !assert.Len(t, logs, 2) { t.Logf(string(data)) t.FailNow() } - require.Contains(t, logs[0], "bar is also not a useful message") - - var entry struct { - Severity string `json:"severity"` - Message string `json:"message"` - } + require.Contains(t, logs[0], "foo is not a useful message") + require.Contains(t, logs[1], "bar is also not a useful message") - err = json.NewDecoder(strings.NewReader(logs[0])).Decode(&entry) + data, err = os.ReadFile(tempJSON) require.NoError(t, err) - require.Equal(t, "INFO", entry.Severity) - require.Equal(t, "bar is also not a useful message", entry.Message) + logs = strings.Split(strings.TrimSpace(string(data)), "\n") + if !assert.Len(t, logs, 2) { + t.Logf(string(data)) + t.FailNow() + } + require.Contains(t, logs[0], "foo is not a useful message") + require.Contains(t, logs[1], "bar is also not a useful 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 From 15e5e1c63f85bc487c69acec14e7b423cad6d165 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 11:51:49 +0000 Subject: [PATCH 3/9] refactor(cli): use clilog instead of BuildLogger --- cli/server.go | 120 +--------------------------------- enterprise/cli/proxyserver.go | 3 +- 2 files changed, 4 insertions(+), 119 deletions(-) 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/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) } From e83c1c01d7f149321c75c77acfb2cf97f7c1615d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 12:30:08 +0000 Subject: [PATCH 4/9] fix(enterprise/cli): plumb through CODER_PROVISIONERD_LOG_* options --- docs/cli/provisionerd_start.md | 47 +++++++++++++ enterprise/cli/provisionerdaemons.go | 68 ++++++++++++++++--- .../coder_provisionerd_start_--help.golden | 16 +++++ 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md index e28a8adafeb05..606cb9f450a3d 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_PROVISIONERD_LOG_FILTER | + +Filter debug logs by matching against a given regex. Use .\* to match all debug logs. + +### --log-human + +| | | +| ----------- | ------------------------------------------ | +| Type | string | +| Environment | $CODER_PROVISIONERD_LOG_HUMAN | +| Default | /dev/stderr | + +Log in human-readable format to the given path. + +### --log-json + +| | | +| ----------- | ----------------------------------------- | +| Type | string | +| Environment | $CODER_PROVISIONERD_LOG_JSON | + +Log in JSON format to the given path. + +### --log-stackdriver + +| | | +| ----------- | ------------------------------------------------ | +| Type | string | +| Environment | $CODER_PROVISIONERD_LOG_STACKDRIVER | + +Log in Stackdriver format to the given path. + ### --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_PROVISIONERD_VERBOSE | +| Default | false | + +Enable verbose logging. This is useful for debugging, but can be noisy when running in production. diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 7200464730841..0a1c839e13cfb 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -13,9 +13,9 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "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 +55,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,10 +94,18 @@ 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) + defer closeLogger() if len(tags) != 0 { logger.Info(ctx, "note: tagged provisioners can currently pick up jobs from untagged templates") @@ -234,6 +247,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/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. From 8249612028fe4c74b939319efee63e3c0b0d6409 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 12:51:57 +0000 Subject: [PATCH 5/9] address linter complaints --- cli/clilog/clilog.go | 2 +- enterprise/cli/provisionerdaemons.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go index 682dd5a4021e1..47990181a82a7 100644 --- a/cli/clilog/clilog.go +++ b/cli/clilog/clilog.go @@ -68,8 +68,8 @@ func WithTrace() Option { return func(b *Builder) { b.Trace = true } - } + func WithVerbose() Option { return func(b *Builder) { b.Verbose = true diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 0a1c839e13cfb..479815996f7cb 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -13,6 +13,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "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" @@ -105,6 +106,11 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { } 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)) + } defer closeLogger() if len(tags) != 0 { From 5120a27a9948a6facfb7a10ff4258d6bee58ff13 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 12:55:17 +0000 Subject: [PATCH 6/9] make fmt --- cli/clilog/clilog.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go index 47990181a82a7..53e66770634ce 100644 --- a/cli/clilog/clilog.go +++ b/cli/clilog/clilog.go @@ -19,15 +19,17 @@ import ( "github.com/coder/coder/v2/codersdk" ) -type Option func(*Builder) -type Builder struct { - Filter []string - Human string - JSON string - Stackdriver string - Trace bool - Verbose bool -} +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{ From 69e13eda07c9a356937bf7b06a05e71edaf19849 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 14:05:36 +0000 Subject: [PATCH 7/9] make gen --- docs/cli/provisionerd_start.md | 52 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md index 606cb9f450a3d..643707f621ae8 100644 --- a/docs/cli/provisionerd_start.md +++ b/docs/cli/provisionerd_start.md @@ -24,40 +24,40 @@ Directory to store cached data. ### --log-filter -| | | -| ----------- | ------------------------------------------- | -| Type | string-array | -| Environment | $CODER_PROVISIONERD_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_PROVISIONERD_LOG_HUMAN | -| Default | /dev/stderr | +| | | +| ----------- | ---------------------------------------------------- | +| Type | string | +| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_HUMAN | +| Default | /dev/stderr | -Log in human-readable format to the given path. +Output human-readable logs to a given file. ### --log-json -| | | -| ----------- | ----------------------------------------- | -| Type | string | -| Environment | $CODER_PROVISIONERD_LOG_JSON | +| | | +| ----------- | --------------------------------------------------- | +| Type | string | +| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_JSON | -Log in JSON format to the given path. +Output JSON logs to a given file. ### --log-stackdriver -| | | -| ----------- | ------------------------------------------------ | -| Type | string | -| Environment | $CODER_PROVISIONERD_LOG_STACKDRIVER | +| | | +| ----------- | ---------------------------------------------------------- | +| Type | string | +| Environment | $CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER | -Log in Stackdriver format to the given path. +Output Stackdriver compatible logs to a given file. ### --name @@ -108,10 +108,10 @@ Tags to filter provisioner jobs by. ### --verbose -| | | -| ----------- | ---------------------------------------- | -| Type | bool | -| Environment | $CODER_PROVISIONERD_VERBOSE | -| Default | false | +| | | +| ----------- | ---------------------------------------------- | +| Type | bool | +| Environment | $CODER_PROVISIONER_DAEMON_VERBOSE | +| Default | false | -Enable verbose logging. This is useful for debugging, but can be noisy when running in production. +Output debug-level logs. From 98b2758d89840f2ee972c10ed665dac2a659db1c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 16:13:39 +0000 Subject: [PATCH 8/9] address PR comments --- cli/clilog/clilog.go | 28 +-- cli/clilog/clilog_test.go | 279 ++++++++++++++++----------- enterprise/cli/provisionerdaemons.go | 3 +- 3 files changed, 189 insertions(+), 121 deletions(-) diff --git a/cli/clilog/clilog.go b/cli/clilog/clilog.go index 53e66770634ce..8bc2799fa8b36 100644 --- a/cli/clilog/clilog.go +++ b/cli/clilog/clilog.go @@ -32,10 +32,7 @@ type ( ) func New(opts ...Option) *Builder { - b := &Builder{ - Human: "/dev/stderr", - Filter: []string{}, - } + b := &Builder{} for _, opt := range opts { opt(b) } @@ -89,11 +86,20 @@ func FromDeploymentValues(vals *codersdk.DeploymentValues) Option { } } -func (b *Builder) Build(inv *clibase.Invocation) (slog.Logger, func(), error) { +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 { @@ -116,17 +122,17 @@ func (b *Builder) Build(inv *clibase.Invocation) (slog.Logger, func(), error) { return nil } - err := addSinkIfProvided(sloghuman.Sink, b.Human) + err = addSinkIfProvided(sloghuman.Sink, b.Human) if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err) + return slog.Logger{}, noopClose, xerrors.Errorf("add human sink: %w", err) } err = addSinkIfProvided(slogjson.Sink, b.JSON) if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err) + return slog.Logger{}, noopClose, xerrors.Errorf("add json sink: %w", err) } err = addSinkIfProvided(slogstackdriver.Sink, b.Stackdriver) if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("add stackdriver sink: %w", err) + return slog.Logger{}, noopClose, xerrors.Errorf("add stackdriver sink: %w", err) } if b.Trace { @@ -135,14 +141,14 @@ func (b *Builder) Build(inv *clibase.Invocation) (slog.Logger, func(), error) { // 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") + 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{}, nil, xerrors.Errorf("compile filters: %w", err) + return slog.Logger{}, noopClose, xerrors.Errorf("compile filters: %w", err) } level := slog.LevelInfo diff --git a/cli/clilog/clilog_test.go b/cli/clilog/clilog_test.go index 321ac055b5372..4851eeff5e87b 100644 --- a/cli/clilog/clilog_test.go +++ b/cli/clilog/clilog_test.go @@ -9,6 +9,7 @@ import ( "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" @@ -18,70 +19,61 @@ import ( func TestBuilder(t *testing.T) { t.Parallel() - t.Run("WithFilter", func(t *testing.T) { + 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: 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.Debug(inv.Context(), "foo is not a useful message") - logger.Debug(inv.Context(), "bar is also not a useful message") - return nil - }, + Handler: testHandler(t, + clilog.WithHuman(tempFile), + clilog.WithVerbose(), + ), } err := cmd.Invoke().Run() require.NoError(t, err) - - data, err := os.ReadFile(tempFile) - require.NoError(t, err) - logs := strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 1) { - t.Logf(string(data)) - t.FailNow() - } - require.Contains(t, logs[0], "foo is not a useful message") + assertLogs(t, tempFile, debugLog, infoLog, warnLog, filterLog) }) - t.Run("WithHuman", func(t *testing.T) { + t.Run("WithFilter", func(t *testing.T) { t.Parallel() tempFile := filepath.Join(t.TempDir(), "test.log") cmd := &clibase.Cmd{ Use: "test", - Handler: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New( - clilog.WithHuman(tempFile)). - Build(inv) - if err != nil { - return err - } - defer closeLog() - logger.Debug(inv.Context(), "foo is not a useful message") - logger.Info(inv.Context(), "bar is also not a useful message") - return nil - }, + 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) + }) - data, err := os.ReadFile(tempFile) - require.NoError(t, err) - logs := strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 1) { - t.Logf(string(data)) - t.FailNow() + 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)), } - require.Contains(t, logs[0], "bar is also not a useful message") + err := cmd.Invoke().Run() + require.NoError(t, err) + assertLogs(t, tempFile, infoLog, warnLog) }) t.Run("WithJSON", func(t *testing.T) { @@ -89,93 +81,162 @@ func TestBuilder(t *testing.T) { tempFile := filepath.Join(t.TempDir(), "test.log") cmd := &clibase.Cmd{ - Use: "test", - Handler: func(inv *clibase.Invocation) error { - logger, closeLog, err := clilog.New( - clilog.WithJSON(tempFile)). - Build(inv) - if err != nil { - return err - } - defer closeLog() - logger.Debug(inv.Context(), "foo is not a useful message") - logger.Info(inv.Context(), "bar is also not a useful message") - return nil - }, + 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) + }) - data, err := os.ReadFile(tempFile) - require.NoError(t, err) - logs := strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 1) { - t.Logf(string(data)) - t.FailNow() - } - require.Contains(t, logs[0], "bar") - var entry struct { - Level string `json:"level"` - Message string `json:"msg"` - } + t.Run("FromDeploymentValues", func(t *testing.T) { + t.Parallel() - err = json.NewDecoder(strings.NewReader(logs[0])).Decode(&entry) - require.NoError(t, err) - require.Equal(t, "INFO", entry.Level) - require.Equal(t, "bar is also not a useful message", entry.Message) + 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("FromDeploymentValues", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { t.Parallel() - 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, - }, - } + 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.FromDeploymentValues(dv)). - Build(inv) + logger, closeLog, err := clilog.New( + clilog.WithFilter("foo", "baz"), + clilog.WithHuman(tempFile), + clilog.WithVerbose(), + ).Build(inv) if err != nil { return err } defer closeLog() - logger.Debug(inv.Context(), "foo is not a useful message") - logger.Info(inv.Context(), "bar is also not a useful message") + logger.Error(inv.Context(), "you will never see this") return nil }, } err := cmd.Invoke().Run() - require.NoError(t, err) + require.ErrorContains(t, err, "no such file or directory") + }) +} - data, err := os.ReadFile(tempFile) - require.NoError(t, err) - logs := strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 2) { - t.Logf(string(data)) - t.FailNow() +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 } - require.Contains(t, logs[0], "foo is not a useful message") - require.Contains(t, logs[1], "bar is also not a useful message") + defer closeLog() + logger.Debug(inv.Context(), debugLog) + logger.Info(inv.Context(), infoLog) + logger.Warn(inv.Context(), warnLog) + logger.Debug(inv.Context(), filterLog) + return nil + } +} - data, err = os.ReadFile(tempJSON) - require.NoError(t, err) - logs = strings.Split(strings.TrimSpace(string(data)), "\n") - if !assert.Len(t, logs, 2) { - t.Logf(string(data)) - t.FailNow() +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"` } - require.Contains(t, logs[0], "foo is not a useful message") - require.Contains(t, logs[1], "bar is also not a useful message") - }) + 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/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 479815996f7cb..576f4f5e8b602 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -110,8 +110,9 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd { // 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() } - defer closeLogger() if len(tags) != 0 { logger.Info(ctx, "note: tagged provisioners can currently pick up jobs from untagged templates") From f2642d0c496841ddd626299b5d7a52dec72191e5 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 19 Dec 2023 16:21:57 +0000 Subject: [PATCH 9/9] fixup! address PR comments --- cli/clilog/clilog_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/clilog/clilog_test.go b/cli/clilog/clilog_test.go index 4851eeff5e87b..f7f854345043e 100644 --- a/cli/clilog/clilog_test.go +++ b/cli/clilog/clilog_test.go @@ -2,6 +2,7 @@ package clilog_test import ( "encoding/json" + "io/fs" "os" "path/filepath" "strings" @@ -166,7 +167,7 @@ func TestBuilder(t *testing.T) { }, } err := cmd.Invoke().Run() - require.ErrorContains(t, err, "no such file or directory") + require.ErrorIs(t, err, fs.ErrNotExist) }) }