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:
Kyle Carberry
2023-03-23 14:09:13 -05:00
committed by GitHub
co-authored by Asher
parent a6fa8cac58
commit cb7375450b
57 changed files with 2513 additions and 353 deletions
+34
View File
@@ -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"`
}
-23
View File
@@ -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 := ""
-11
View File
@@ -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
View File
@@ -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"`
}
-5
View File
@@ -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)