fix(cli)!: protect client Logger and refactor cli scaletest tests (#8317)

- (breaking) Protects Logger and LogBodies fields of codersdk.Client with its mutex. This addresses a data race in cli/scaletest.
- Fillets the existing cli/createworkspaces unit test and moves the testing logic there into the tests under scaletest/createworkspaces.
- Adds testutil.RaceEnabled bool const and conditionaly skips previously-skipped tests under scaletest/ if the race detector is enabled. This is unfortunate and sad, but I would prefer to have these tests at least running without the race detector than not running at all.
- Adds IgnoreErrors option to fake in-memory agent loggers; having the agents fail the test immediately when they encounter any sort of error isn't really helpful.
This commit is contained in:
Cian Johnston
2023-07-06 09:43:39 +01:00
committed by GitHub
parent 1d746b901b
commit 7fcf319e01
20 changed files with 309 additions and 254 deletions

View File

@ -194,7 +194,7 @@ func (c *Client) Listen(ctx context.Context) (net.Conn, error) {
ticker := time.NewTicker(tick)
defer ticker.Stop()
defer func() {
c.SDK.Logger.Debug(ctx, "coordinate pinger exited")
c.SDK.Logger().Debug(ctx, "coordinate pinger exited")
}()
for {
select {
@ -205,18 +205,18 @@ func (c *Client) Listen(ctx context.Context) (net.Conn, error) {
err := conn.Ping(ctx)
if err != nil {
c.SDK.Logger.Error(ctx, "workspace agent coordinate ping", slog.Error(err))
c.SDK.Logger().Error(ctx, "workspace agent coordinate ping", slog.Error(err))
err := conn.Close(websocket.StatusGoingAway, "Ping failed")
if err != nil {
c.SDK.Logger.Error(ctx, "close workspace agent coordinate websocket", slog.Error(err))
c.SDK.Logger().Error(ctx, "close workspace agent coordinate websocket", slog.Error(err))
}
cancel()
return
}
c.SDK.Logger.Debug(ctx, "got coordinate pong", slog.F("took", time.Since(start)))
c.SDK.Logger().Debug(ctx, "got coordinate pong", slog.F("took", time.Since(start)))
cancel()
}
}

View File

@ -93,8 +93,12 @@ func New(serverURL *url.URL) *Client {
// Client is an HTTP caller for methods to the Coder API.
// @typescript-ignore Client
type Client struct {
mu sync.RWMutex // Protects following.
// mu protects the fields sessionToken, logger, and logBodies. These
// need to be safe for concurrent access.
mu sync.RWMutex
sessionToken string
logger slog.Logger
logBodies bool
HTTPClient *http.Client
URL *url.URL
@ -103,13 +107,6 @@ type Client struct {
// default 'Coder-Session-Token' is used.
SessionTokenHeader string
// Logger is optionally provided to log requests.
// Method, URL, and response code will be logged by default.
Logger slog.Logger
// LogBodies can be enabled to print request and response bodies to the logger.
LogBodies bool
// PlainLogger may be set to log HTTP traffic in a human-readable form.
// It uses the LogBodies option.
PlainLogger io.Writer
@ -124,6 +121,34 @@ type Client struct {
DisableDirectConnections bool
}
// Logger returns the logger for the client.
func (c *Client) Logger() slog.Logger {
c.mu.RLock()
defer c.mu.RUnlock()
return c.logger
}
// SetLogger sets the logger for the client.
func (c *Client) SetLogger(logger slog.Logger) {
c.mu.Lock()
defer c.mu.Unlock()
c.logger = logger
}
// LogBodies returns whether requests and response bodies are logged.
func (c *Client) LogBodies() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.logBodies
}
// SetLogBodies sets whether to log request and response bodies.
func (c *Client) SetLogBodies(logBodies bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.logBodies = logBodies
}
// SessionToken returns the currently set token for the client.
func (c *Client) SessionToken() string {
c.mu.RLock()
@ -181,7 +206,10 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
// Copy the request body so we can log it.
var reqBody []byte
if r != nil && c.LogBodies {
c.mu.RLock()
logBodies := c.logBodies
c.mu.RUnlock()
if r != nil && logBodies {
reqBody, err = io.ReadAll(r)
if err != nil {
return nil, xerrors.Errorf("read request body: %w", err)
@ -223,7 +251,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
slog.F("url", req.URL.String()),
)
tracing.RunWithoutSpan(ctx, func(ctx context.Context) {
c.Logger.Debug(ctx, "sdk request", slog.F("body", string(reqBody)))
c.Logger().Debug(ctx, "sdk request", slog.F("body", string(reqBody)))
})
resp, err := c.HTTPClient.Do(req)
@ -231,7 +259,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
// We log after sending the request because the HTTP Transport may modify
// the request within Do, e.g. by adding headers.
if resp != nil && c.PlainLogger != nil {
out, err := httputil.DumpRequest(resp.Request, c.LogBodies)
out, err := httputil.DumpRequest(resp.Request, logBodies)
if err != nil {
return nil, xerrors.Errorf("dump request: %w", err)
}
@ -244,7 +272,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
}
if c.PlainLogger != nil {
out, err := httputil.DumpResponse(resp, c.LogBodies)
out, err := httputil.DumpResponse(resp, logBodies)
if err != nil {
return nil, xerrors.Errorf("dump response: %w", err)
}
@ -257,7 +285,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
// Copy the response body so we can log it if it's a loggable mime type.
var respBody []byte
if resp.Body != nil && c.LogBodies {
if resp.Body != nil && logBodies {
mimeType := parseMimeType(resp.Header.Get("Content-Type"))
if _, ok := loggableMimeTypes[mimeType]; ok {
respBody, err = io.ReadAll(resp.Body)
@ -274,7 +302,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
// See above for why this is not logged to the span.
tracing.RunWithoutSpan(ctx, func(ctx context.Context) {
c.Logger.Debug(ctx, "sdk response",
c.Logger().Debug(ctx, "sdk response",
slog.F("status", resp.StatusCode),
slog.F("body", string(respBody)),
slog.F("trace_id", resp.Header.Get("X-Trace-Id")),

View File

@ -114,8 +114,8 @@ func Test_Client(t *testing.T) {
client.SetSessionToken(token)
logBuf := bytes.NewBuffer(nil)
client.Logger = slog.Make(sloghuman.Sink(logBuf)).Leveled(slog.LevelDebug)
client.LogBodies = true
client.SetLogger(slog.Make(sloghuman.Sink(logBuf)).Leveled(slog.LevelDebug))
client.SetLogBodies(true)
// Setup tracing.
res := resource.NewWithAttributes(