mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
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:
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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")),
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user