mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add startup script logs to the ui (#6558)
* Add startup script logs to the database * Add coderd endpoints for startup script logs * Push startup script logs from agent * Pull startup script logs on frontend * Rename queries * Add constraint * Start creating log sending loop * Add log sending to the agent * Add tests for streaming logs * Shorten notify channel name * Add FE * Improve bulk log performance * Finish UI display * Fix startup log visibility * Add warning for overflow * Fix agent queue logs overflow * Display staartup logs in a virtual DOM for performance * Fix agent queue with loads of logs * Fix authorize test * Remove faulty test * Fix startup and shutdown reporting error * Fix gen * Fix comments * Periodically purge old database entries * Add test fixture for migration * Add Storybook * Check if there are logs when displaying features * Fix startup component overflow gap * Fix startup log wrapping --------- Co-authored-by: Asher <ash@coder.com>
This commit is contained in:
@@ -516,6 +516,29 @@ func (c *Client) PostStartup(ctx context.Context, req PostStartupRequest) error
|
||||
return nil
|
||||
}
|
||||
|
||||
type StartupLog struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
type PatchStartupLogs struct {
|
||||
Logs []StartupLog `json:"logs"`
|
||||
}
|
||||
|
||||
// PatchStartupLogs writes log messages to the agent startup script.
|
||||
// Log messages are limited to 1MB in total.
|
||||
func (c *Client) PatchStartupLogs(ctx context.Context, req PatchStartupLogs) error {
|
||||
res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/startup-logs", req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GitAuthResponse struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
@@ -589,3 +612,14 @@ func websocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websock
|
||||
Conn: nc,
|
||||
}
|
||||
}
|
||||
|
||||
// StartupLogsNotifyChannel returns the channel name responsible for notifying
|
||||
// of new startup logs.
|
||||
func StartupLogsNotifyChannel(agentID uuid.UUID) string {
|
||||
return fmt.Sprintf("startup-logs:%s", agentID)
|
||||
}
|
||||
|
||||
type StartupLogsNotifyMessage struct {
|
||||
CreatedAfter int64 `json:"created_after"`
|
||||
EndOfLogs bool `json:"end_of_logs"`
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -100,27 +98,6 @@ type ProvisionerJobLog struct {
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// provisionerJobLogsBefore provides log output that occurred before a time.
|
||||
// This is abstracted from a specific job type to provide consistency between
|
||||
// APIs. Logs is the only shared route between jobs.
|
||||
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before int64) ([]ProvisionerJobLog, error) {
|
||||
values := url.Values{}
|
||||
if before != 0 {
|
||||
values["before"] = []string{strconv.FormatInt(before, 10)}
|
||||
}
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
defer res.Body.Close()
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var logs []ProvisionerJobLog
|
||||
return logs, json.NewDecoder(res.Body).Decode(&logs)
|
||||
}
|
||||
|
||||
// provisionerJobLogsAfter streams logs that occurred after a specific time.
|
||||
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after int64) (<-chan ProvisionerJobLog, io.Closer, error) {
|
||||
afterQuery := ""
|
||||
|
||||
@@ -191,11 +191,6 @@ func (c *Client) TemplateVersionVariables(ctx context.Context, version uuid.UUID
|
||||
return variables, json.NewDecoder(res.Body).Decode(&variables)
|
||||
}
|
||||
|
||||
// TemplateVersionLogsBefore returns logs that occurred before a specific log ID.
|
||||
func (c *Client) TemplateVersionLogsBefore(ctx context.Context, version uuid.UUID, before int64) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/templateversions/%s/logs", version), before)
|
||||
}
|
||||
|
||||
// TemplateVersionLogsAfter streams logs for a template version that occurred after a specific log ID.
|
||||
func (c *Client) TemplateVersionLogsAfter(ctx context.Context, version uuid.UUID, after int64) (<-chan ProvisionerJobLog, io.Closer, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/templateversions/%s/logs", version), after)
|
||||
@@ -258,12 +253,6 @@ func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, jo
|
||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
||||
}
|
||||
|
||||
// TemplateVersionDryRunLogsBefore returns logs for a template version dry-run
|
||||
// that occurred before a specific log ID.
|
||||
func (c *Client) TemplateVersionDryRunLogsBefore(ctx context.Context, version, job uuid.UUID, before int64) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dry-run/%s/logs", version, job), before)
|
||||
}
|
||||
|
||||
// TemplateVersionDryRunLogsAfter streams logs for a template version dry-run
|
||||
// that occurred after a specific log ID.
|
||||
func (c *Client) TemplateVersionDryRunLogsAfter(ctx context.Context, version, job uuid.UUID, after int64) (<-chan ProvisionerJobLog, io.Closer, error) {
|
||||
|
||||
+87
-19
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -74,25 +75,27 @@ var WorkspaceAgentLifecycleOrder = []WorkspaceAgentLifecycle{
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"`
|
||||
LastConnectedAt *time.Time `json:"last_connected_at,omitempty" format:"date-time"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at,omitempty" format:"date-time"`
|
||||
Status WorkspaceAgentStatus `json:"status"`
|
||||
LifecycleState WorkspaceAgentLifecycle `json:"lifecycle_state"`
|
||||
Name string `json:"name"`
|
||||
ResourceID uuid.UUID `json:"resource_id" format:"uuid"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
Architecture string `json:"architecture"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
StartupScript string `json:"startup_script,omitempty"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
ExpandedDirectory string `json:"expanded_directory,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Apps []WorkspaceApp `json:"apps"`
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"`
|
||||
LastConnectedAt *time.Time `json:"last_connected_at,omitempty" format:"date-time"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at,omitempty" format:"date-time"`
|
||||
Status WorkspaceAgentStatus `json:"status"`
|
||||
LifecycleState WorkspaceAgentLifecycle `json:"lifecycle_state"`
|
||||
Name string `json:"name"`
|
||||
ResourceID uuid.UUID `json:"resource_id" format:"uuid"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
Architecture string `json:"architecture"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
StartupScript string `json:"startup_script,omitempty"`
|
||||
StartupLogsLength int32 `json:"startup_logs_length"`
|
||||
StartupLogsOverflowed bool `json:"startup_logs_overflowed"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
ExpandedDirectory string `json:"expanded_directory,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Apps []WorkspaceApp `json:"apps"`
|
||||
// DERPLatency is mapped by region name (e.g. "New York City", "Seattle").
|
||||
DERPLatency map[string]DERPRegion `json:"latency,omitempty"`
|
||||
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
|
||||
@@ -322,6 +325,65 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.
|
||||
return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uuid.UUID, after int64) (<-chan []WorkspaceAgentStartupLog, io.Closer, error) {
|
||||
afterQuery := ""
|
||||
if after != 0 {
|
||||
afterQuery = fmt.Sprintf("&after=%d", after)
|
||||
}
|
||||
followURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/startup-logs?follow%s", agentID, afterQuery))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
|
||||
}
|
||||
jar.SetCookies(followURL, []*http.Cookie{{
|
||||
Name: SessionTokenCookie,
|
||||
Value: c.SessionToken(),
|
||||
}})
|
||||
httpClient := &http.Client{
|
||||
Jar: jar,
|
||||
Transport: c.HTTPClient.Transport,
|
||||
}
|
||||
conn, res, err := websocket.Dial(ctx, followURL.String(), &websocket.DialOptions{
|
||||
HTTPClient: httpClient,
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, ReadBodyAsError(res)
|
||||
}
|
||||
logChunks := make(chan []WorkspaceAgentStartupLog)
|
||||
closed := make(chan struct{})
|
||||
ctx, wsNetConn := websocketNetConn(ctx, conn, websocket.MessageText)
|
||||
decoder := json.NewDecoder(wsNetConn)
|
||||
go func() {
|
||||
defer close(closed)
|
||||
defer close(logChunks)
|
||||
defer conn.Close(websocket.StatusGoingAway, "")
|
||||
var logs []WorkspaceAgentStartupLog
|
||||
for {
|
||||
err = decoder.Decode(&logs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case logChunks <- logs:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return logChunks, closeFunc(func() error {
|
||||
_ = wsNetConn.Close()
|
||||
<-closed
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// GitProvider is a constant that represents the
|
||||
// type of providers that are supported within Coder.
|
||||
type GitProvider string
|
||||
@@ -347,3 +409,9 @@ const (
|
||||
GitProviderGitLab GitProvider = "gitlab"
|
||||
GitProviderBitBucket GitProvider = "bitbucket"
|
||||
)
|
||||
|
||||
type WorkspaceAgentStartupLog struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
@@ -130,11 +130,6 @@ func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsBefore returns logs that occurred before a specific log ID.
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, build uuid.UUID, before int64) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), before)
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsAfter streams logs for a workspace build that occurred after a specific log ID.
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, build uuid.UUID, after int64) (<-chan ProvisionerJobLog, io.Closer, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), after)
|
||||
|
||||
Reference in New Issue
Block a user