Skip to content

Commit 4449931

Browse files
authored
chore: reduce log volume on server startup (coder#16608)
Addresses coder#16231. This PR reduces the volume of logs we print after server startup in order to surface the web UI URL better. Here are the logs after the changes a couple of seconds after starting the server: <img width="868" alt="Screenshot 2025-02-18 at 16 31 32" src="https://github.com/user-attachments/assets/786dc4b8-7383-48c8-a5c3-a997c01ca915" /> The warning is due to running a development site-less build. It wouldn't show in a release build.
1 parent 54b09d9 commit 4449931

File tree

12 files changed

+207
-61
lines changed

12 files changed

+207
-61
lines changed

cli/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
938938
notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal())
939939
defer notificationReportGenerator.Close()
940940
} else {
941-
cliui.Info(inv.Stdout, "Notifications are currently disabled as there are no configured delivery methods. See https://coder.com/docs/admin/monitoring/notifications#delivery-methods for more details.")
941+
logger.Debug(ctx, "notifications are currently disabled as there are no configured delivery methods. See https://coder.com/docs/admin/monitoring/notifications#delivery-methods for more details")
942942
}
943943

944944
// Since errCh only has one buffered slot, all routines

cli/server_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"runtime"
2626
"strconv"
2727
"strings"
28+
"sync"
2829
"sync/atomic"
2930
"testing"
3031
"time"
@@ -240,6 +241,70 @@ func TestServer(t *testing.T) {
240241
t.Fatalf("expected postgres URL to start with \"postgres://\", got %q", got)
241242
}
242243
})
244+
t.Run("SpammyLogs", func(t *testing.T) {
245+
// The purpose of this test is to ensure we don't show excessive logs when the server starts.
246+
t.Parallel()
247+
inv, cfg := clitest.New(t,
248+
"server",
249+
"--in-memory",
250+
"--http-address", ":0",
251+
"--access-url", "http://localhost:3000/",
252+
"--cache-dir", t.TempDir(),
253+
)
254+
stdoutRW := syncReaderWriter{}
255+
stderrRW := syncReaderWriter{}
256+
inv.Stdout = io.MultiWriter(os.Stdout, &stdoutRW)
257+
inv.Stderr = io.MultiWriter(os.Stderr, &stderrRW)
258+
clitest.Start(t, inv)
259+
260+
// Wait for startup
261+
_ = waitAccessURL(t, cfg)
262+
263+
// Wait a bit for more logs to be printed.
264+
time.Sleep(testutil.WaitShort)
265+
266+
// Lines containing these strings are printed because we're
267+
// running the server with a test config. They wouldn't be
268+
// normally shown to the user, so we'll ignore them.
269+
ignoreLines := []string{
270+
"isn't externally reachable",
271+
"install.sh will be unavailable",
272+
"telemetry disabled, unable to notify of security issues",
273+
}
274+
275+
countLines := func(fullOutput string) int {
276+
terminalWidth := 80
277+
linesByNewline := strings.Split(fullOutput, "\n")
278+
countByWidth := 0
279+
lineLoop:
280+
for _, line := range linesByNewline {
281+
for _, ignoreLine := range ignoreLines {
282+
if strings.Contains(line, ignoreLine) {
283+
continue lineLoop
284+
}
285+
}
286+
if line == "" {
287+
// Empty lines take up one line.
288+
countByWidth++
289+
} else {
290+
countByWidth += (len(line) + terminalWidth - 1) / terminalWidth
291+
}
292+
}
293+
return countByWidth
294+
}
295+
296+
stdout, err := io.ReadAll(&stdoutRW)
297+
if err != nil {
298+
t.Fatalf("failed to read stdout: %v", err)
299+
}
300+
stderr, err := io.ReadAll(&stderrRW)
301+
if err != nil {
302+
t.Fatalf("failed to read stderr: %v", err)
303+
}
304+
305+
numLines := countLines(string(stdout)) + countLines(string(stderr))
306+
require.Less(t, numLines, 20)
307+
})
243308

244309
// Validate that a warning is printed that it may not be externally
245310
// reachable.
@@ -2140,3 +2205,22 @@ func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, ch
21402205

21412206
return serverURL, deployment, snapshot
21422207
}
2208+
2209+
// syncWriter provides a thread-safe io.ReadWriter implementation
2210+
type syncReaderWriter struct {
2211+
buf bytes.Buffer
2212+
mu sync.Mutex
2213+
}
2214+
2215+
func (w *syncReaderWriter) Write(p []byte) (n int, err error) {
2216+
w.mu.Lock()
2217+
defer w.mu.Unlock()
2218+
return w.buf.Write(p)
2219+
}
2220+
2221+
func (w *syncReaderWriter) Read(p []byte) (n int, err error) {
2222+
w.mu.Lock()
2223+
defer w.mu.Unlock()
2224+
2225+
return w.buf.Read(p)
2226+
}

coderd/cryptokeys/rotate.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (k *rotator) rotateKeys(ctx context.Context) error {
152152
}
153153
}
154154
if validKeys == 0 {
155-
k.logger.Info(ctx, "no valid keys detected, inserting new key",
155+
k.logger.Debug(ctx, "no valid keys detected, inserting new key",
156156
slog.F("feature", feature),
157157
)
158158
_, err := k.insertNewKey(ctx, tx, feature, now)
@@ -194,7 +194,7 @@ func (k *rotator) insertNewKey(ctx context.Context, tx database.Store, feature d
194194
return database.CryptoKey{}, xerrors.Errorf("inserting new key: %w", err)
195195
}
196196

197-
k.logger.Info(ctx, "inserted new key for feature", slog.F("feature", feature))
197+
k.logger.Debug(ctx, "inserted new key for feature", slog.F("feature", feature))
198198
return newKey, nil
199199
}
200200

coderd/database/dbpurge/dbpurge.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, clk quartz.
6363
return xerrors.Errorf("failed to delete old notification messages: %w", err)
6464
}
6565

66-
logger.Info(ctx, "purged old database entries", slog.F("duration", clk.Since(start)))
66+
logger.Debug(ctx, "purged old database entries", slog.F("duration", clk.Since(start)))
6767

6868
return nil
6969
}, database.DefaultTXOptions().WithID("db_purge")); err != nil {

enterprise/cli/provisionerdaemonstart.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,11 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
236236
ProvisionerKey: provisionerKey,
237237
})
238238
}, &provisionerd.Options{
239-
Logger: logger,
240-
UpdateInterval: 500 * time.Millisecond,
241-
Connector: connector,
242-
Metrics: metrics,
239+
Logger: logger,
240+
UpdateInterval: 500 * time.Millisecond,
241+
Connector: connector,
242+
Metrics: metrics,
243+
ExternalProvisioner: true,
243244
})
244245

245246
waitForProvisionerJobs := false

provisioner/terraform/install.go

+38-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package terraform
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
8+
"sync/atomic"
79
"time"
810

911
"github.com/gofrs/flock"
@@ -30,7 +32,9 @@ var (
3032

3133
// Install implements a thread-safe, idempotent Terraform Install
3234
// operation.
33-
func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *version.Version) (string, error) {
35+
//
36+
//nolint:revive // verbose is a control flag that controls the verbosity of the log output.
37+
func Install(ctx context.Context, log slog.Logger, verbose bool, dir string, wantVersion *version.Version) (string, error) {
3438
err := os.MkdirAll(dir, 0o750)
3539
if err != nil {
3640
return "", err
@@ -64,13 +68,37 @@ func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *vers
6468
Version: TerraformVersion,
6569
}
6670
installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
67-
log.Debug(
68-
ctx,
69-
"installing terraform",
71+
72+
logInstall := log.Debug
73+
if verbose {
74+
logInstall = log.Info
75+
}
76+
77+
logInstall(ctx, "installing terraform",
7078
slog.F("prev_version", hasVersionStr),
7179
slog.F("dir", dir),
72-
slog.F("version", TerraformVersion),
73-
)
80+
slog.F("version", TerraformVersion))
81+
82+
prolongedInstall := atomic.Bool{}
83+
prolongedInstallCtx, prolongedInstallCancel := context.WithCancel(ctx)
84+
go func() {
85+
seconds := 15
86+
select {
87+
case <-time.After(time.Duration(seconds) * time.Second):
88+
prolongedInstall.Store(true)
89+
// We always want to log this at the info level.
90+
log.Info(
91+
prolongedInstallCtx,
92+
fmt.Sprintf("terraform installation is taking longer than %d seconds, still in progress", seconds),
93+
slog.F("prev_version", hasVersionStr),
94+
slog.F("dir", dir),
95+
slog.F("version", TerraformVersion),
96+
)
97+
case <-prolongedInstallCtx.Done():
98+
return
99+
}
100+
}()
101+
defer prolongedInstallCancel()
74102

75103
path, err := installer.Install(ctx)
76104
if err != nil {
@@ -83,5 +111,9 @@ func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *vers
83111
return "", xerrors.Errorf("%s should be %s", path, binPath)
84112
}
85113

114+
if prolongedInstall.Load() {
115+
log.Info(ctx, "terraform installation complete")
116+
}
117+
86118
return path, nil
87119
}

provisioner/terraform/install_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func TestInstall(t *testing.T) {
4040
wg.Add(1)
4141
go func() {
4242
defer wg.Done()
43-
p, err := terraform.Install(ctx, log, dir, version)
43+
p, err := terraform.Install(ctx, log, false, dir, version)
4444
assert.NoError(t, err)
4545
paths <- p
4646
}()

provisioner/terraform/serve.go

+44-28
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package terraform
22

33
import (
44
"context"
5+
"errors"
56
"path/filepath"
67
"sync"
78
"time"
89

910
"github.com/cli/safeexec"
11+
"github.com/hashicorp/go-version"
1012
semconv "go.opentelemetry.io/otel/semconv/v1.14.0"
1113
"go.opentelemetry.io/otel/trace"
1214
"golang.org/x/xerrors"
@@ -41,10 +43,15 @@ type ServeOptions struct {
4143
ExitTimeout time.Duration
4244
}
4345

44-
func absoluteBinaryPath(ctx context.Context, logger slog.Logger) (string, error) {
46+
type systemBinaryDetails struct {
47+
absolutePath string
48+
version *version.Version
49+
}
50+
51+
func systemBinary(ctx context.Context) (*systemBinaryDetails, error) {
4552
binaryPath, err := safeexec.LookPath("terraform")
4653
if err != nil {
47-
return "", xerrors.Errorf("Terraform binary not found: %w", err)
54+
return nil, xerrors.Errorf("Terraform binary not found: %w", err)
4855
}
4956

5057
// If the "coder" binary is in the same directory as
@@ -54,59 +61,68 @@ func absoluteBinaryPath(ctx context.Context, logger slog.Logger) (string, error)
5461
// to execute this properly!
5562
absoluteBinary, err := filepath.Abs(binaryPath)
5663
if err != nil {
57-
return "", xerrors.Errorf("Terraform binary absolute path not found: %w", err)
64+
return nil, xerrors.Errorf("Terraform binary absolute path not found: %w", err)
5865
}
5966

6067
// Checking the installed version of Terraform.
6168
installedVersion, err := versionFromBinaryPath(ctx, absoluteBinary)
6269
if err != nil {
63-
return "", xerrors.Errorf("Terraform binary get version failed: %w", err)
70+
return nil, xerrors.Errorf("Terraform binary get version failed: %w", err)
6471
}
6572

66-
logger.Info(ctx, "detected terraform version",
67-
slog.F("installed_version", installedVersion.String()),
68-
slog.F("min_version", minTerraformVersion.String()),
69-
slog.F("max_version", maxTerraformVersion.String()))
70-
71-
if installedVersion.LessThan(minTerraformVersion) {
72-
logger.Warn(ctx, "installed terraform version too old, will download known good version to cache")
73-
return "", terraformMinorVersionMismatch
73+
details := &systemBinaryDetails{
74+
absolutePath: absoluteBinary,
75+
version: installedVersion,
7476
}
7577

76-
// Warn if the installed version is newer than what we've decided is the max.
77-
// We used to ignore it and download our own version but this makes it easier
78-
// to test out newer versions of Terraform.
79-
if installedVersion.GreaterThanOrEqual(maxTerraformVersion) {
80-
logger.Warn(ctx, "installed terraform version newer than expected, you may experience bugs",
81-
slog.F("installed_version", installedVersion.String()),
82-
slog.F("max_version", maxTerraformVersion.String()))
78+
if installedVersion.LessThan(minTerraformVersion) {
79+
return details, terraformMinorVersionMismatch
8380
}
8481

85-
return absoluteBinary, nil
82+
return details, nil
8683
}
8784

8885
// Serve starts a dRPC server on the provided transport speaking Terraform provisioner.
8986
func Serve(ctx context.Context, options *ServeOptions) error {
9087
if options.BinaryPath == "" {
91-
absoluteBinary, err := absoluteBinaryPath(ctx, options.Logger)
88+
binaryDetails, err := systemBinary(ctx)
9289
if err != nil {
9390
// This is an early exit to prevent extra execution in case the context is canceled.
9491
// It generally happens in unit tests since this method is asynchronous and
9592
// the unit test kills the app before this is complete.
96-
if xerrors.Is(err, context.Canceled) {
97-
return xerrors.Errorf("absolute binary context canceled: %w", err)
93+
if errors.Is(err, context.Canceled) {
94+
return xerrors.Errorf("system binary context canceled: %w", err)
9895
}
9996

100-
options.Logger.Warn(ctx, "no usable terraform binary found, downloading to cache dir",
101-
slog.F("terraform_version", TerraformVersion.String()),
102-
slog.F("cache_dir", options.CachePath))
103-
binPath, err := Install(ctx, options.Logger, options.CachePath, TerraformVersion)
97+
if errors.Is(err, terraformMinorVersionMismatch) {
98+
options.Logger.Warn(ctx, "installed terraform version too old, will download known good version to cache, or use a previously cached version",
99+
slog.F("installed_version", binaryDetails.version.String()),
100+
slog.F("min_version", minTerraformVersion.String()))
101+
}
102+
103+
binPath, err := Install(ctx, options.Logger, options.ExternalProvisioner, options.CachePath, TerraformVersion)
104104
if err != nil {
105105
return xerrors.Errorf("install terraform: %w", err)
106106
}
107107
options.BinaryPath = binPath
108108
} else {
109-
options.BinaryPath = absoluteBinary
109+
logVersion := options.Logger.Debug
110+
if options.ExternalProvisioner {
111+
logVersion = options.Logger.Info
112+
}
113+
logVersion(ctx, "detected terraform version",
114+
slog.F("installed_version", binaryDetails.version.String()),
115+
slog.F("min_version", minTerraformVersion.String()),
116+
slog.F("max_version", maxTerraformVersion.String()))
117+
// Warn if the installed version is newer than what we've decided is the max.
118+
// We used to ignore it and download our own version but this makes it easier
119+
// to test out newer versions of Terraform.
120+
if binaryDetails.version.GreaterThanOrEqual(maxTerraformVersion) {
121+
options.Logger.Warn(ctx, "installed terraform version newer than expected, you may experience bugs",
122+
slog.F("installed_version", binaryDetails.version.String()),
123+
slog.F("max_version", maxTerraformVersion.String()))
124+
}
125+
options.BinaryPath = binaryDetails.absolutePath
110126
}
111127
}
112128
if options.Tracer == nil {

provisioner/terraform/serve_internal_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ func Test_absoluteBinaryPath(t *testing.T) {
5454
t.Skip("Dummy terraform executable on Windows requires sh which isn't very practical.")
5555
}
5656

57-
log := testutil.Logger(t)
5857
// Create a temp dir with the binary
5958
tempDir := t.TempDir()
6059
terraformBinaryOutput := fmt.Sprintf(`#!/bin/sh
@@ -85,11 +84,12 @@ func Test_absoluteBinaryPath(t *testing.T) {
8584
}
8685

8786
ctx := testutil.Context(t, testutil.WaitShort)
88-
actualAbsoluteBinary, actualErr := absoluteBinaryPath(ctx, log)
87+
actualBinaryDetails, actualErr := systemBinary(ctx)
8988

90-
require.Equal(t, expectedAbsoluteBinary, actualAbsoluteBinary)
9189
if tt.expectedErr == nil {
9290
require.NoError(t, actualErr)
91+
require.Equal(t, expectedAbsoluteBinary, actualBinaryDetails.absolutePath)
92+
require.Equal(t, tt.terraformVersion, actualBinaryDetails.version.String())
9393
} else {
9494
require.EqualError(t, actualErr, tt.expectedErr.Error())
9595
}

0 commit comments

Comments
 (0)