chore: rename startup logs to agent logs (#8649)

* chore: rename startup logs to agent logs

This also adds a `source` property to every agent log. It
should allow us to group logs and display them nicer in
the UI as they stream in.

* Fix migration order

* Fix naming

* Rename the frontend

* Fix tests

* Fix down migration

* Match enums for workspace agent logs

* Fix inserting log source

* Fix migration order

* Fix logs tests

* Fix psql insert
This commit is contained in:
Kyle Carberry 2023-07-28 09:57:23 -06:00 committed by GitHub
parent 78b06397a6
commit bd944e0d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1471 additions and 876 deletions

View File

@ -79,7 +79,7 @@ type Client interface {
PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error
PostStartup(ctx context.Context, req agentsdk.PostStartupRequest) error
PostMetadata(ctx context.Context, key string, req agentsdk.PostMetadataRequest) error
PatchStartupLogs(ctx context.Context, req agentsdk.PatchStartupLogs) error
PatchLogs(ctx context.Context, req agentsdk.PatchLogs) error
GetServiceBanner(ctx context.Context) (codersdk.ServiceBannerConfig, error)
}
@ -1006,7 +1006,7 @@ func (a *agent) runScript(ctx context.Context, lifecycle, script string) (err er
var stdout, stderr io.Writer = fileWriter, fileWriter
if lifecycle == "startup" {
send, flushAndClose := agentsdk.StartupLogsSender(a.client.PatchStartupLogs, logger)
send, flushAndClose := agentsdk.LogsSender(a.client.PatchLogs, logger)
// If ctx is canceled here (or in a writer below), we may be
// discarding logs, but that's okay because we're shutting down
// anyway. We could consider creating a new context here if we
@ -1017,9 +1017,9 @@ func (a *agent) runScript(ctx context.Context, lifecycle, script string) (err er
}
}()
infoW := agentsdk.StartupLogsWriter(ctx, send, codersdk.LogLevelInfo)
infoW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelInfo)
defer infoW.Close()
errW := agentsdk.StartupLogsWriter(ctx, send, codersdk.LogLevelError)
errW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelError)
defer errW.Close()
stdout = io.MultiWriter(fileWriter, infoW)

View File

@ -55,7 +55,7 @@ type Client struct {
mu sync.Mutex // Protects following.
lifecycleStates []codersdk.WorkspaceAgentLifecycle
startup agentsdk.PostStartupRequest
logs []agentsdk.StartupLog
logs []agentsdk.Log
derpMapUpdates chan agentsdk.DERPMapUpdate
}
@ -161,13 +161,13 @@ func (c *Client) PostStartup(ctx context.Context, startup agentsdk.PostStartupRe
return nil
}
func (c *Client) GetStartupLogs() []agentsdk.StartupLog {
func (c *Client) GetStartupLogs() []agentsdk.Log {
c.mu.Lock()
defer c.mu.Unlock()
return c.logs
}
func (c *Client) PatchStartupLogs(ctx context.Context, logs agentsdk.PatchStartupLogs) error {
func (c *Client) PatchLogs(ctx context.Context, logs agentsdk.PatchLogs) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.PatchWorkspaceLogs != nil {

View File

@ -16,7 +16,7 @@ var errAgentShuttingDown = xerrors.New("agent is shutting down")
type AgentOptions struct {
FetchInterval time.Duration
Fetch func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error)
FetchLogs func(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []codersdk.WorkspaceAgentStartupLog, io.Closer, error)
FetchLogs func(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error)
Wait bool // If true, wait for the agent to be ready (startup script).
}
@ -29,8 +29,8 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
opts.FetchInterval = 500 * time.Millisecond
}
if opts.FetchLogs == nil {
opts.FetchLogs = func(_ context.Context, _ uuid.UUID, _ int64, _ bool) (<-chan []codersdk.WorkspaceAgentStartupLog, io.Closer, error) {
c := make(chan []codersdk.WorkspaceAgentStartupLog)
opts.FetchLogs = func(_ context.Context, _ uuid.UUID, _ int64, _ bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
c := make(chan []codersdk.WorkspaceAgentLog)
close(c)
return c, closeFunc(func() error { return nil }), nil
}
@ -137,7 +137,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
}
defer logsCloser.Close()
var lastLog codersdk.WorkspaceAgentStartupLog
var lastLog codersdk.WorkspaceAgentLog
fetchedAgentWhileFollowing := fetchedAgent
if !follow {
fetchedAgentWhileFollowing = nil

View File

@ -27,8 +27,8 @@ func TestAgent(t *testing.T) {
for _, tc := range []struct {
name string
iter []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error
logs chan []codersdk.WorkspaceAgentStartupLog
iter []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error
logs chan []codersdk.WorkspaceAgentLog
opts cliui.AgentOptions
want []string
wantErr bool
@ -38,12 +38,12 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
return nil
@ -62,18 +62,18 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
@ -95,8 +95,8 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.FirstConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
agent.LastConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
@ -106,7 +106,7 @@ func TestAgent(t *testing.T) {
agent.ReadyAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.LastConnectedAt = ptr.Ref(time.Now())
return nil
@ -125,13 +125,13 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentStartupLog{
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
@ -139,10 +139,10 @@ func TestAgent(t *testing.T) {
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
agent.ReadyAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentStartupLog{
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Bye now",
@ -164,14 +164,14 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.StartedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
agent.ReadyAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentStartupLog{
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
@ -193,8 +193,8 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleOff
return nil
@ -208,13 +208,13 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentStartupLog{
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
@ -222,7 +222,7 @@ func TestAgent(t *testing.T) {
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.ReadyAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleShuttingDown
return nil
@ -241,12 +241,12 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
@ -261,13 +261,13 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentStartupLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
agent.TroubleshootingURL = "https://troubleshoot"
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentStartupLog) error {
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
@ -294,7 +294,7 @@ func TestAgent(t *testing.T) {
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
}
logs := make(chan []codersdk.WorkspaceAgentStartupLog, 1)
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
@ -306,12 +306,12 @@ func TestAgent(t *testing.T) {
}
return agent, err
}
tc.opts.FetchLogs = func(ctx context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentStartupLog, io.Closer, error) {
tc.opts.FetchLogs = func(ctx context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
if follow {
return logs, closeFunc(func() error { return nil }), nil
}
fetchLogs := make(chan []codersdk.WorkspaceAgentStartupLog, 1)
fetchLogs := make(chan []codersdk.WorkspaceAgentLog, 1)
select {
case <-ctx.Done():
return nil, nil, ctx.Err()

View File

@ -186,7 +186,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
// This is required in "stdio" mode so a connecting indicator can be displayed.
err = cliui.Agent(ctx, inv.Stderr, workspaceAgent.ID, cliui.AgentOptions{
Fetch: client.WorkspaceAgent,
FetchLogs: client.WorkspaceAgentStartupLogsAfter,
FetchLogs: client.WorkspaceAgentLogsAfter,
Wait: wait,
})
if err != nil {

View File

@ -167,7 +167,7 @@ func main() {
Use: "agent",
Handler: func(inv *clibase.Invocation) error {
var agent codersdk.WorkspaceAgent
var logs []codersdk.WorkspaceAgentStartupLog
var logs []codersdk.WorkspaceAgentLog
fetchSteps := []func(){
func() {
@ -191,7 +191,7 @@ func main() {
if rand.Float64() > 0.75 { //nolint:gosec
level = codersdk.LogLevelError
}
logs = append(logs, codersdk.WorkspaceAgentStartupLog{
logs = append(logs, codersdk.WorkspaceAgentLog{
CreatedAt: time.Now().Add(-time.Duration(10-i) * 144 * time.Millisecond),
Output: fmt.Sprintf("Some log %d", i),
Level: level,
@ -226,13 +226,13 @@ func main() {
step()
return agent, nil
},
FetchLogs: func(_ context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentStartupLog, io.Closer, error) {
logsC := make(chan []codersdk.WorkspaceAgentStartupLog, len(logs))
FetchLogs: func(_ context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
logsC := make(chan []codersdk.WorkspaceAgentLog, len(logs))
if follow {
go func() {
defer close(logsC)
for _, log := range logs {
logsC <- []codersdk.WorkspaceAgentStartupLog{log}
logsC <- []codersdk.WorkspaceAgentLog{log}
time.Sleep(144 * time.Millisecond)
}
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady

217
coderd/apidoc/docs.go generated
View File

@ -4565,6 +4565,45 @@ const docTemplate = `{
}
}
},
"/workspaceagents/me/logs": {
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Patch workspace agent logs",
"operationId": "patch-workspace-agent-logs",
"parameters": [
{
"description": "logs",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PatchLogs"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/workspaceagents/me/manifest": {
"get": {
"security": [
@ -4764,16 +4803,16 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Patch workspace agent startup logs",
"operationId": "patch-workspace-agent-startup-logs",
"summary": "Removed: Patch workspace agent logs",
"operationId": "removed-patch-workspace-agent-logs",
"parameters": [
{
"description": "Startup logs",
"description": "logs",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PatchStartupLogs"
"$ref": "#/definitions/agentsdk.PatchLogs"
}
}
],
@ -4959,6 +4998,68 @@ const docTemplate = `{
}
}
},
"/workspaceagents/{workspaceagent}/logs": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Get logs by workspace agent",
"operationId": "get-logs-by-workspace-agent",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Before log id",
"name": "before",
"in": "query"
},
{
"type": "integer",
"description": "After log id",
"name": "after",
"in": "query"
},
{
"type": "boolean",
"description": "Follow log stream",
"name": "follow",
"in": "query"
},
{
"type": "boolean",
"description": "Disable compression for WebSocket connection",
"name": "no_compression",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLog"
}
}
}
}
}
},
"/workspaceagents/{workspaceagent}/pty": {
"get": {
"security": [
@ -5001,8 +5102,8 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get startup logs by workspace agent",
"operationId": "get-startup-logs-by-workspace-agent",
"summary": "Removed: Get logs by workspace agent",
"operationId": "removed-get-logs-by-workspace-agent",
"parameters": [
{
"type": "string",
@ -5043,7 +5144,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupLog"
"$ref": "#/definitions/codersdk.WorkspaceAgentLog"
}
}
}
@ -6213,6 +6314,23 @@ const docTemplate = `{
}
}
},
"agentsdk.Log": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
},
"source": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
}
},
"agentsdk.Manifest": {
"type": "object",
"properties": {
@ -6270,13 +6388,13 @@ const docTemplate = `{
}
}
},
"agentsdk.PatchStartupLogs": {
"agentsdk.PatchLogs": {
"type": "object",
"properties": {
"logs": {
"type": "array",
"items": {
"$ref": "#/definitions/agentsdk.StartupLog"
"$ref": "#/definitions/agentsdk.Log"
}
}
}
@ -6337,20 +6455,6 @@ const docTemplate = `{
}
}
},
"agentsdk.StartupLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"agentsdk.Stats": {
"type": "object",
"properties": {
@ -10325,6 +10429,12 @@ const docTemplate = `{
"description": "Deprecated: Use StartupScriptBehavior instead.",
"type": "boolean"
},
"logs_length": {
"type": "integer"
},
"logs_overflowed": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -10349,12 +10459,6 @@ const docTemplate = `{
"type": "string",
"format": "date-time"
},
"startup_logs_length": {
"type": "integer"
},
"startup_logs_overflowed": {
"type": "boolean"
},
"startup_script": {
"type": "string"
},
@ -10462,6 +10566,43 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceAgentLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"codersdk.WorkspaceAgentLogSource": {
"type": "string",
"enum": [
"startup_script",
"shutdown_script",
"kubernetes",
"envbox",
"envbuilder",
"external"
],
"x-enum-varnames": [
"WorkspaceAgentLogSourceStartupScript",
"WorkspaceAgentLogSourceShutdownScript",
"WorkspaceAgentLogSourceKubernetes",
"WorkspaceAgentLogSourceEnvbox",
"WorkspaceAgentLogSourceEnvbuilder",
"WorkspaceAgentLogSourceExternal"
]
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
"properties": {
@ -10482,24 +10623,6 @@ const docTemplate = `{
}
}
},
"codersdk.WorkspaceAgentStartupLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"codersdk.WorkspaceAgentStartupScriptBehavior": {
"type": "string",
"enum": [

View File

@ -4017,6 +4017,39 @@
}
}
},
"/workspaceagents/me/logs": {
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Agents"],
"summary": "Patch workspace agent logs",
"operationId": "patch-workspace-agent-logs",
"parameters": [
{
"description": "logs",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PatchLogs"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/workspaceagents/me/manifest": {
"get": {
"security": [
@ -4186,16 +4219,16 @@
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Agents"],
"summary": "Patch workspace agent startup logs",
"operationId": "patch-workspace-agent-startup-logs",
"summary": "Removed: Patch workspace agent logs",
"operationId": "removed-patch-workspace-agent-logs",
"parameters": [
{
"description": "Startup logs",
"description": "logs",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/agentsdk.PatchStartupLogs"
"$ref": "#/definitions/agentsdk.PatchLogs"
}
}
],
@ -4363,34 +4396,7 @@
}
}
},
"/workspaceagents/{workspaceagent}/pty": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Agents"],
"summary": "Open PTY to workspace agent",
"operationId": "open-pty-to-workspace-agent",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/workspaceagents/{workspaceagent}/startup-logs": {
"/workspaceagents/{workspaceagent}/logs": {
"get": {
"security": [
{
@ -4399,8 +4405,8 @@
],
"produces": ["application/json"],
"tags": ["Agents"],
"summary": "Get startup logs by workspace agent",
"operationId": "get-startup-logs-by-workspace-agent",
"summary": "Get logs by workspace agent",
"operationId": "get-logs-by-workspace-agent",
"parameters": [
{
"type": "string",
@ -4441,7 +4447,92 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStartupLog"
"$ref": "#/definitions/codersdk.WorkspaceAgentLog"
}
}
}
}
}
},
"/workspaceagents/{workspaceagent}/pty": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Agents"],
"summary": "Open PTY to workspace agent",
"operationId": "open-pty-to-workspace-agent",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/workspaceagents/{workspaceagent}/startup-logs": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Agents"],
"summary": "Removed: Get logs by workspace agent",
"operationId": "removed-get-logs-by-workspace-agent",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace agent ID",
"name": "workspaceagent",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Before log id",
"name": "before",
"in": "query"
},
{
"type": "integer",
"description": "After log id",
"name": "after",
"in": "query"
},
{
"type": "boolean",
"description": "Follow log stream",
"name": "follow",
"in": "query"
},
{
"type": "boolean",
"description": "Disable compression for WebSocket connection",
"name": "no_compression",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLog"
}
}
}
@ -5473,6 +5564,23 @@
}
}
},
"agentsdk.Log": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
},
"source": {
"$ref": "#/definitions/codersdk.WorkspaceAgentLogSource"
}
}
},
"agentsdk.Manifest": {
"type": "object",
"properties": {
@ -5530,13 +5638,13 @@
}
}
},
"agentsdk.PatchStartupLogs": {
"agentsdk.PatchLogs": {
"type": "object",
"properties": {
"logs": {
"type": "array",
"items": {
"$ref": "#/definitions/agentsdk.StartupLog"
"$ref": "#/definitions/agentsdk.Log"
}
}
}
@ -5597,20 +5705,6 @@
}
}
},
"agentsdk.StartupLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"agentsdk.Stats": {
"type": "object",
"properties": {
@ -9354,6 +9448,12 @@
"description": "Deprecated: Use StartupScriptBehavior instead.",
"type": "boolean"
},
"logs_length": {
"type": "integer"
},
"logs_overflowed": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -9378,12 +9478,6 @@
"type": "string",
"format": "date-time"
},
"startup_logs_length": {
"type": "integer"
},
"startup_logs_overflowed": {
"type": "boolean"
},
"startup_script": {
"type": "string"
},
@ -9491,6 +9585,43 @@
}
}
},
"codersdk.WorkspaceAgentLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"codersdk.WorkspaceAgentLogSource": {
"type": "string",
"enum": [
"startup_script",
"shutdown_script",
"kubernetes",
"envbox",
"envbuilder",
"external"
],
"x-enum-varnames": [
"WorkspaceAgentLogSourceStartupScript",
"WorkspaceAgentLogSourceShutdownScript",
"WorkspaceAgentLogSourceKubernetes",
"WorkspaceAgentLogSourceEnvbox",
"WorkspaceAgentLogSourceEnvbuilder",
"WorkspaceAgentLogSourceExternal"
]
},
"codersdk.WorkspaceAgentMetadataDescription": {
"type": "object",
"properties": {
@ -9511,24 +9642,6 @@
}
}
},
"codersdk.WorkspaceAgentStartupLog": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer"
},
"level": {
"$ref": "#/definitions/codersdk.LogLevel"
},
"output": {
"type": "string"
}
}
},
"codersdk.WorkspaceAgentStartupScriptBehavior": {
"type": "string",
"enum": ["blocking", "non-blocking"],

View File

@ -776,7 +776,8 @@ func New(options *Options) *API {
// New agents will use /me/manifest instead.
r.Get("/metadata", api.workspaceAgentManifest)
r.Post("/startup", api.postWorkspaceAgentStartup)
r.Patch("/startup-logs", api.patchWorkspaceAgentStartupLogs)
r.Patch("/startup-logs", api.patchWorkspaceAgentLogsDeprecated)
r.Patch("/logs", api.patchWorkspaceAgentLogs)
r.Post("/app-health", api.postWorkspaceAppHealth)
r.Get("/gitauth", api.workspaceAgentsGitAuth)
r.Get("/gitsshkey", api.agentGitSSHKey)
@ -800,7 +801,8 @@ func New(options *Options) *API {
)
r.Get("/", api.workspaceAgent)
r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata)
r.Get("/startup-logs", api.workspaceAgentStartupLogs)
r.Get("/startup-logs", api.workspaceAgentLogsDeprecated)
r.Get("/logs", api.workspaceAgentLogs)
r.Get("/listening-ports", api.workspaceAgentListeningPorts)
r.Get("/connection", api.workspaceAgentConnection)
r.Get("/coordinate", api.workspaceAgentClientCoordinate)

View File

@ -722,11 +722,11 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
return id, nil
}
func (q *querier) DeleteOldWorkspaceAgentStartupLogs(ctx context.Context) error {
func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error {
if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceSystem); err != nil {
return err
}
return q.db.DeleteOldWorkspaceAgentStartupLogs(ctx)
return q.db.DeleteOldWorkspaceAgentLogs(ctx)
}
func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
@ -1444,6 +1444,14 @@ func (q *querier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uu
return q.db.GetWorkspaceAgentLifecycleStateByID(ctx, id)
}
func (q *querier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
_, err := q.GetWorkspaceAgentByID(ctx, arg.AgentID)
if err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentLogsAfter(ctx, arg)
}
func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
workspace, err := q.db.GetWorkspaceByAgentID(ctx, workspaceAgentID)
if err != nil {
@ -1458,14 +1466,6 @@ func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentI
return q.db.GetWorkspaceAgentMetadata(ctx, workspaceAgentID)
}
func (q *querier) GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) {
_, err := q.GetWorkspaceAgentByID(ctx, arg.AgentID)
if err != nil {
return nil, err
}
return q.db.GetWorkspaceAgentStartupLogsAfter(ctx, arg)
}
func (q *querier) GetWorkspaceAgentStats(ctx context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
return q.db.GetWorkspaceAgentStats(ctx, createdAfter)
}
@ -1926,6 +1926,10 @@ func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertW
return q.db.InsertWorkspaceAgent(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
return q.db.InsertWorkspaceAgentLogs(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error {
// We don't check for workspace ownership here since the agent metadata may
// be associated with an orphaned agent used by a dry run build.
@ -1936,10 +1940,6 @@ func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database
return q.db.InsertWorkspaceAgentMetadata(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentStartupLogs(ctx context.Context, arg database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) {
return q.db.InsertWorkspaceAgentStartupLogs(ctx, arg)
}
func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
// TODO: This is a workspace agent operation. Should users be able to query this?
// Not really sure what this is for.
@ -2404,6 +2404,24 @@ func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, ar
return q.db.UpdateWorkspaceAgentLifecycleStateByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentLogOverflowByIDParams) error {
agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID)
if err != nil {
return err
}
workspace, err := q.db.GetWorkspaceByAgentID(ctx, agent.ID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, workspace); err != nil {
return err
}
return q.db.UpdateWorkspaceAgentLogOverflowByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error {
workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID)
if err != nil {
@ -2436,24 +2454,6 @@ func (q *querier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg datab
return q.db.UpdateWorkspaceAgentStartupByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentStartupLogOverflowByIDParams) error {
agent, err := q.db.GetWorkspaceAgentByID(ctx, arg.ID)
if err != nil {
return err
}
workspace, err := q.db.GetWorkspaceByAgentID(ctx, agent.ID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, rbac.ActionUpdate, workspace); err != nil {
return err
}
return q.db.UpdateWorkspaceAgentStartupLogOverflowByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error {
// TODO: This is a workspace agent operation. Should users be able to query this?
workspace, err := q.db.GetWorkspaceByWorkspaceAppID(ctx, arg.ID)

View File

@ -1053,14 +1053,14 @@ func (s *MethodTestSuite) TestWorkspace() {
LifecycleState: database.WorkspaceAgentLifecycleStateCreated,
}).Asserts(ws, rbac.ActionUpdate).Returns()
}))
s.Run("UpdateWorkspaceAgentStartupLogOverflowByID", s.Subtest(func(db database.Store, check *expects) {
s.Run("UpdateWorkspaceAgentLogOverflowByID", s.Subtest(func(db database.Store, check *expects) {
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()})
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID})
agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
check.Args(database.UpdateWorkspaceAgentStartupLogOverflowByIDParams{
ID: agt.ID,
StartupLogsOverflowed: true,
check.Args(database.UpdateWorkspaceAgentLogOverflowByIDParams{
ID: agt.ID,
LogsOverflowed: true,
}).Asserts(ws, rbac.ActionUpdate).Returns()
}))
s.Run("UpdateWorkspaceAgentStartupByID", s.Subtest(func(db database.Store, check *expects) {
@ -1073,14 +1073,14 @@ func (s *MethodTestSuite) TestWorkspace() {
Subsystem: database.WorkspaceAgentSubsystemNone,
}).Asserts(ws, rbac.ActionUpdate).Returns()
}))
s.Run("GetWorkspaceAgentStartupLogsAfter", s.Subtest(func(db database.Store, check *expects) {
s.Run("GetWorkspaceAgentLogsAfter", s.Subtest(func(db database.Store, check *expects) {
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()})
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID})
agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
check.Args(database.GetWorkspaceAgentStartupLogsAfterParams{
check.Args(database.GetWorkspaceAgentLogsAfterParams{
AgentID: agt.ID,
}).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceAgentStartupLog{})
}).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceAgentLog{})
}))
s.Run("GetWorkspaceAppByAgentIDAndSlug", s.Subtest(func(db database.Store, check *expects) {
ws := dbgen.Workspace(s.T(), db, database.Workspace{})

View File

@ -60,7 +60,7 @@ func New() database.Store {
templateVersions: make([]database.TemplateVersionTable, 0),
templates: make([]database.TemplateTable, 0),
workspaceAgentStats: make([]database.WorkspaceAgentStat, 0),
workspaceAgentLogs: make([]database.WorkspaceAgentStartupLog, 0),
workspaceAgentLogs: make([]database.WorkspaceAgentLog, 0),
workspaceBuilds: make([]database.WorkspaceBuildTable, 0),
workspaceApps: make([]database.WorkspaceApp, 0),
workspaces: make([]database.Workspace, 0),
@ -133,7 +133,7 @@ type data struct {
templates []database.TemplateTable
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentStartupLog
workspaceAgentLogs []database.WorkspaceAgentLog
workspaceApps []database.WorkspaceApp
workspaceBuilds []database.WorkspaceBuildTable
workspaceBuildParameters []database.WorkspaceBuildParameter
@ -789,7 +789,7 @@ func (q *FakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error)
return 0, sql.ErrNoRows
}
func (*FakeQuerier) DeleteOldWorkspaceAgentStartupLogs(_ context.Context) error {
func (*FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context) error {
// noop
return nil
}
@ -2640,6 +2640,27 @@ func (q *FakeQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, i
}, nil
}
func (q *FakeQuerier) GetWorkspaceAgentLogsAfter(_ context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
}
q.mutex.RLock()
defer q.mutex.RUnlock()
logs := []database.WorkspaceAgentLog{}
for _, log := range q.workspaceAgentLogs {
if log.AgentID != arg.AgentID {
continue
}
if arg.CreatedAfter != 0 && log.ID <= arg.CreatedAfter {
continue
}
logs = append(logs, log)
}
return logs, nil
}
func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -2653,27 +2674,6 @@ func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgen
return metadata, nil
}
func (q *FakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
}
q.mutex.RLock()
defer q.mutex.RUnlock()
logs := []database.WorkspaceAgentStartupLog{}
for _, log := range q.workspaceAgentLogs {
if log.AgentID != arg.AgentID {
continue
}
if arg.CreatedAfter != 0 && log.ID <= arg.CreatedAfter {
continue
}
logs = append(logs, log)
}
return logs, nil
}
func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -3964,6 +3964,51 @@ func (q *FakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
return agent, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentLogs(_ context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
logs := []database.WorkspaceAgentLog{}
id := int64(0)
if len(q.workspaceAgentLogs) > 0 {
id = q.workspaceAgentLogs[len(q.workspaceAgentLogs)-1].ID
}
outputLength := int32(0)
for index, output := range arg.Output {
id++
logs = append(logs, database.WorkspaceAgentLog{
ID: id,
AgentID: arg.AgentID,
CreatedAt: arg.CreatedAt[index],
Level: arg.Level[index],
Source: arg.Source[index],
Output: output,
})
outputLength += int32(len(output))
}
for index, agent := range q.workspaceAgents {
if agent.ID != arg.AgentID {
continue
}
// Greater than 1MB, same as the PostgreSQL constraint!
if agent.LogsLength+outputLength > (1 << 20) {
return nil, &pq.Error{
Constraint: "max_logs_length",
Table: "workspace_agents",
}
}
agent.LogsLength += outputLength
q.workspaceAgents[index] = agent
break
}
q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...)
return logs, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg database.InsertWorkspaceAgentMetadataParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -3982,50 +4027,6 @@ func (q *FakeQuerier) InsertWorkspaceAgentMetadata(_ context.Context, arg databa
return nil
}
func (q *FakeQuerier) InsertWorkspaceAgentStartupLogs(_ context.Context, arg database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
logs := []database.WorkspaceAgentStartupLog{}
id := int64(0)
if len(q.workspaceAgentLogs) > 0 {
id = q.workspaceAgentLogs[len(q.workspaceAgentLogs)-1].ID
}
outputLength := int32(0)
for index, output := range arg.Output {
id++
logs = append(logs, database.WorkspaceAgentStartupLog{
ID: id,
AgentID: arg.AgentID,
CreatedAt: arg.CreatedAt[index],
Level: arg.Level[index],
Output: output,
})
outputLength += int32(len(output))
}
for index, agent := range q.workspaceAgents {
if agent.ID != arg.AgentID {
continue
}
// Greater than 1MB, same as the PostgreSQL constraint!
if agent.StartupLogsLength+outputLength > (1 << 20) {
return nil, &pq.Error{
Constraint: "max_startup_logs_length",
Table: "workspace_agents",
}
}
agent.StartupLogsLength += outputLength
q.workspaceAgents[index] = agent
break
}
q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...)
return logs, nil
}
func (q *FakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
if err := validateDatabaseType(p); err != nil {
return database.WorkspaceAgentStat{}, err
@ -4923,6 +4924,23 @@ func (q *FakeQuerier) UpdateWorkspaceAgentLifecycleStateByID(_ context.Context,
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAgentLogOverflowByID(_ context.Context, arg database.UpdateWorkspaceAgentLogOverflowByIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, agent := range q.workspaceAgents {
if agent.ID == arg.ID {
agent.LogsOverflowed = arg.LogsOverflowed
q.workspaceAgents[i] = agent
return nil
}
}
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAgentMetadata(_ context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -4968,23 +4986,6 @@ func (q *FakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg dat
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAgentStartupLogOverflowByID(_ context.Context, arg database.UpdateWorkspaceAgentStartupLogOverflowByIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, agent := range q.workspaceAgents {
if agent.ID == arg.ID {
agent.StartupLogsOverflowed = arg.StartupLogsOverflowed
q.workspaceAgents[i] = agent
return nil
}
}
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAppHealthByID(_ context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error {
if err := validateDatabaseType(arg); err != nil {
return err

View File

@ -162,11 +162,11 @@ func (m metricsStore) DeleteLicense(ctx context.Context, id int32) (int32, error
return licenseID, err
}
func (m metricsStore) DeleteOldWorkspaceAgentStartupLogs(ctx context.Context) error {
func (m metricsStore) DeleteOldWorkspaceAgentLogs(ctx context.Context) error {
start := time.Now()
err := m.s.DeleteOldWorkspaceAgentStartupLogs(ctx)
m.queryLatencies.WithLabelValues("DeleteOldWorkspaceAgentStartupLogs").Observe(time.Since(start).Seconds())
return err
r0 := m.s.DeleteOldWorkspaceAgentLogs(ctx)
m.queryLatencies.WithLabelValues("DeleteOldWorkspaceAgentLogs").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
@ -781,6 +781,13 @@ func (m metricsStore) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, i
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentLogsAfter(ctx, arg)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentLogsAfter").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
start := time.Now()
metadata, err := m.s.GetWorkspaceAgentMetadata(ctx, workspaceAgentID)
@ -788,13 +795,6 @@ func (m metricsStore) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAg
return metadata, err
}
func (m metricsStore) GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) {
start := time.Now()
logs, err := m.s.GetWorkspaceAgentStartupLogsAfter(ctx, arg)
m.queryLatencies.WithLabelValues("GetWorkspaceAgentStartupLogsAfter").Observe(time.Since(start).Seconds())
return logs, err
}
func (m metricsStore) GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
start := time.Now()
stats, err := m.s.GetWorkspaceAgentStats(ctx, createdAt)
@ -1194,6 +1194,13 @@ func (m metricsStore) InsertWorkspaceAgent(ctx context.Context, arg database.Ins
return agent, err
}
func (m metricsStore) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceAgentLogs(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentLogs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error {
start := time.Now()
err := m.s.InsertWorkspaceAgentMetadata(ctx, arg)
@ -1201,13 +1208,6 @@ func (m metricsStore) InsertWorkspaceAgentMetadata(ctx context.Context, arg data
return err
}
func (m metricsStore) InsertWorkspaceAgentStartupLogs(ctx context.Context, arg database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) {
start := time.Now()
logs, err := m.s.InsertWorkspaceAgentStartupLogs(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentStartupLogs").Observe(time.Since(start).Seconds())
return logs, err
}
func (m metricsStore) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
start := time.Now()
stat, err := m.s.InsertWorkspaceAgentStat(ctx, arg)
@ -1481,6 +1481,13 @@ func (m metricsStore) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context
return r0
}
func (m metricsStore) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentLogOverflowByIDParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspaceAgentLogOverflowByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceAgentLogOverflowByID").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceAgentMetadata(ctx, arg)
@ -1495,13 +1502,6 @@ func (m metricsStore) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg d
return err
}
func (m metricsStore) UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentStartupLogOverflowByIDParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceAgentStartupLogOverflowByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceAgentStartupLogOverflowByID").Observe(time.Since(start).Seconds())
return err
}
func (m metricsStore) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database.UpdateWorkspaceAppHealthByIDParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceAppHealthByID(ctx, arg)

View File

@ -209,18 +209,18 @@ func (mr *MockStoreMockRecorder) DeleteLicense(arg0, arg1 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLicense", reflect.TypeOf((*MockStore)(nil).DeleteLicense), arg0, arg1)
}
// DeleteOldWorkspaceAgentStartupLogs mocks base method.
func (m *MockStore) DeleteOldWorkspaceAgentStartupLogs(arg0 context.Context) error {
// DeleteOldWorkspaceAgentLogs mocks base method.
func (m *MockStore) DeleteOldWorkspaceAgentLogs(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOldWorkspaceAgentStartupLogs", arg0)
ret := m.ctrl.Call(m, "DeleteOldWorkspaceAgentLogs", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOldWorkspaceAgentStartupLogs indicates an expected call of DeleteOldWorkspaceAgentStartupLogs.
func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStartupLogs(arg0 interface{}) *gomock.Call {
// DeleteOldWorkspaceAgentLogs indicates an expected call of DeleteOldWorkspaceAgentLogs.
func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentLogs(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentStartupLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentStartupLogs), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentLogs), arg0)
}
// DeleteOldWorkspaceAgentStats mocks base method.
@ -1616,6 +1616,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentLifecycleStateByID(arg0, arg1
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLifecycleStateByID), arg0, arg1)
}
// GetWorkspaceAgentLogsAfter mocks base method.
func (m *MockStore) GetWorkspaceAgentLogsAfter(arg0 context.Context, arg1 database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentLogsAfter", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentLog)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentLogsAfter indicates an expected call of GetWorkspaceAgentLogsAfter.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogsAfter(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLogsAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLogsAfter), arg0, arg1)
}
// GetWorkspaceAgentMetadata mocks base method.
func (m *MockStore) GetWorkspaceAgentMetadata(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) {
m.ctrl.T.Helper()
@ -1631,21 +1646,6 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1)
}
// GetWorkspaceAgentStartupLogsAfter mocks base method.
func (m *MockStore) GetWorkspaceAgentStartupLogsAfter(arg0 context.Context, arg1 database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceAgentStartupLogsAfter", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentStartupLog)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceAgentStartupLogsAfter indicates an expected call of GetWorkspaceAgentStartupLogsAfter.
func (mr *MockStoreMockRecorder) GetWorkspaceAgentStartupLogsAfter(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentStartupLogsAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentStartupLogsAfter), arg0, arg1)
}
// GetWorkspaceAgentStats mocks base method.
func (m *MockStore) GetWorkspaceAgentStats(arg0 context.Context, arg1 time.Time) ([]database.GetWorkspaceAgentStatsRow, error) {
m.ctrl.T.Helper()
@ -2509,6 +2509,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgent(arg0, arg1 interface{}) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgent", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgent), arg0, arg1)
}
// InsertWorkspaceAgentLogs mocks base method.
func (m *MockStore) InsertWorkspaceAgentLogs(arg0 context.Context, arg1 database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentLogs", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentLog)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentLogs indicates an expected call of InsertWorkspaceAgentLogs.
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogs(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentLogs", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentLogs), arg0, arg1)
}
// InsertWorkspaceAgentMetadata mocks base method.
func (m *MockStore) InsertWorkspaceAgentMetadata(arg0 context.Context, arg1 database.InsertWorkspaceAgentMetadataParams) error {
m.ctrl.T.Helper()
@ -2523,21 +2538,6 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentMetadata(arg0, arg1 interfa
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentMetadata), arg0, arg1)
}
// InsertWorkspaceAgentStartupLogs mocks base method.
func (m *MockStore) InsertWorkspaceAgentStartupLogs(arg0 context.Context, arg1 database.InsertWorkspaceAgentStartupLogsParams) ([]database.WorkspaceAgentStartupLog, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceAgentStartupLogs", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceAgentStartupLog)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceAgentStartupLogs indicates an expected call of InsertWorkspaceAgentStartupLogs.
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStartupLogs(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStartupLogs", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStartupLogs), arg0, arg1)
}
// InsertWorkspaceAgentStat mocks base method.
func (m *MockStore) InsertWorkspaceAgentStat(arg0 context.Context, arg1 database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
m.ctrl.T.Helper()
@ -3120,6 +3120,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLifecycleStateByID(arg0, ar
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentLifecycleStateByID), arg0, arg1)
}
// UpdateWorkspaceAgentLogOverflowByID mocks base method.
func (m *MockStore) UpdateWorkspaceAgentLogOverflowByID(arg0 context.Context, arg1 database.UpdateWorkspaceAgentLogOverflowByIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceAgentLogOverflowByID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspaceAgentLogOverflowByID indicates an expected call of UpdateWorkspaceAgentLogOverflowByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLogOverflowByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentLogOverflowByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentLogOverflowByID), arg0, arg1)
}
// UpdateWorkspaceAgentMetadata mocks base method.
func (m *MockStore) UpdateWorkspaceAgentMetadata(arg0 context.Context, arg1 database.UpdateWorkspaceAgentMetadataParams) error {
m.ctrl.T.Helper()
@ -3148,20 +3162,6 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentStartupByID(arg0, arg1 inte
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentStartupByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentStartupByID), arg0, arg1)
}
// UpdateWorkspaceAgentStartupLogOverflowByID mocks base method.
func (m *MockStore) UpdateWorkspaceAgentStartupLogOverflowByID(arg0 context.Context, arg1 database.UpdateWorkspaceAgentStartupLogOverflowByIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceAgentStartupLogOverflowByID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspaceAgentStartupLogOverflowByID indicates an expected call of UpdateWorkspaceAgentStartupLogOverflowByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentStartupLogOverflowByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentStartupLogOverflowByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentStartupLogOverflowByID), arg0, arg1)
}
// UpdateWorkspaceAppHealthByID mocks base method.
func (m *MockStore) UpdateWorkspaceAppHealthByID(arg0 context.Context, arg1 database.UpdateWorkspaceAppHealthByIDParams) error {
m.ctrl.T.Helper()

View File

@ -40,7 +40,7 @@ func New(ctx context.Context, logger slog.Logger, db database.Store) io.Closer {
var eg errgroup.Group
eg.Go(func() error {
return db.DeleteOldWorkspaceAgentStartupLogs(ctx)
return db.DeleteOldWorkspaceAgentLogs(ctx)
})
eg.Go(func() error {
return db.DeleteOldWorkspaceAgentStats(ctx)

View File

@ -128,6 +128,15 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
'off'
);
CREATE TYPE workspace_agent_log_source AS ENUM (
'startup_script',
'shutdown_script',
'kubernetes_logs',
'envbox',
'envbuilder',
'external'
);
CREATE TYPE workspace_agent_subsystem AS ENUM (
'envbuilder',
'envbox',
@ -672,6 +681,15 @@ CREATE TABLE user_links (
oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE workspace_agent_logs (
agent_id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
output character varying(1024) NOT NULL,
id bigint NOT NULL,
level log_level DEFAULT 'info'::log_level NOT NULL,
source workspace_agent_log_source DEFAULT 'startup_script'::workspace_agent_log_source NOT NULL
);
CREATE UNLOGGED TABLE workspace_agent_metadata (
workspace_agent_id uuid NOT NULL,
display_name character varying(127) NOT NULL,
@ -684,14 +702,6 @@ CREATE UNLOGGED TABLE workspace_agent_metadata (
collected_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE workspace_agent_startup_logs (
agent_id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
output character varying(1024) NOT NULL,
id bigint NOT NULL,
level log_level DEFAULT 'info'::log_level NOT NULL
);
CREATE SEQUENCE workspace_agent_startup_logs_id_seq
START WITH 1
INCREMENT BY 1
@ -699,7 +709,7 @@ CREATE SEQUENCE workspace_agent_startup_logs_id_seq
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE workspace_agent_startup_logs_id_seq OWNED BY workspace_agent_startup_logs.id;
ALTER SEQUENCE workspace_agent_startup_logs_id_seq OWNED BY workspace_agent_logs.id;
CREATE TABLE workspace_agent_stats (
id uuid NOT NULL,
@ -749,13 +759,13 @@ CREATE TABLE workspace_agents (
expanded_directory character varying(4096) DEFAULT ''::character varying NOT NULL,
shutdown_script character varying(65534),
shutdown_script_timeout_seconds integer DEFAULT 0 NOT NULL,
startup_logs_length integer DEFAULT 0 NOT NULL,
startup_logs_overflowed boolean DEFAULT false NOT NULL,
logs_length integer DEFAULT 0 NOT NULL,
logs_overflowed boolean DEFAULT false NOT NULL,
subsystem workspace_agent_subsystem DEFAULT 'none'::workspace_agent_subsystem NOT NULL,
startup_script_behavior startup_script_behavior DEFAULT 'non-blocking'::startup_script_behavior NOT NULL,
started_at timestamp with time zone,
ready_at timestamp with time zone,
CONSTRAINT max_startup_logs_length CHECK ((startup_logs_length <= 1048576))
CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576))
);
COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.';
@ -776,9 +786,9 @@ COMMENT ON COLUMN workspace_agents.shutdown_script IS 'Script that is executed b
COMMENT ON COLUMN workspace_agents.shutdown_script_timeout_seconds IS 'The number of seconds to wait for the shutdown script to complete. If the script does not complete within this time, the agent lifecycle will be marked as shutdown_timeout.';
COMMENT ON COLUMN workspace_agents.startup_logs_length IS 'Total length of startup logs';
COMMENT ON COLUMN workspace_agents.logs_length IS 'Total length of startup logs';
COMMENT ON COLUMN workspace_agents.startup_logs_overflowed IS 'Whether the startup logs overflowed in length';
COMMENT ON COLUMN workspace_agents.logs_overflowed IS 'Whether the startup logs overflowed in length';
COMMENT ON COLUMN workspace_agents.startup_script_behavior IS 'When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible.';
@ -938,7 +948,7 @@ ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq':
ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass);
ALTER TABLE ONLY workspace_agent_startup_logs ALTER COLUMN id SET DEFAULT nextval('workspace_agent_startup_logs_id_seq'::regclass);
ALTER TABLE ONLY workspace_agent_logs ALTER COLUMN id SET DEFAULT nextval('workspace_agent_startup_logs_id_seq'::regclass);
ALTER TABLE ONLY workspace_proxies ALTER COLUMN region_id SET DEFAULT nextval('workspace_proxies_region_id_seq'::regclass);
@ -1046,7 +1056,7 @@ ALTER TABLE ONLY users
ALTER TABLE ONLY workspace_agent_metadata
ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
ALTER TABLE ONLY workspace_agent_startup_logs
ALTER TABLE ONLY workspace_agent_logs
ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agents
@ -1132,7 +1142,7 @@ CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WH
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
CREATE INDEX workspace_agent_startup_logs_id_agent_id_idx ON workspace_agent_startup_logs USING btree (agent_id, id);
CREATE INDEX workspace_agent_startup_logs_id_agent_id_idx ON workspace_agent_logs USING btree (agent_id, id);
CREATE INDEX workspace_agents_auth_token_idx ON workspace_agents USING btree (auth_token);
@ -1217,7 +1227,7 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY workspace_agent_metadata
ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_startup_logs
ALTER TABLE ONLY workspace_agent_logs
ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agents

View File

@ -49,10 +49,10 @@ func IsQueryCanceledError(err error) bool {
return false
}
func IsStartupLogsLimitError(err error) bool {
func IsWorkspaceAgentLogsLimitError(err error) bool {
var pqErr *pq.Error
if errors.As(err, &pqErr) {
return pqErr.Constraint == "max_startup_logs_length" && pqErr.Table == "workspace_agents"
return pqErr.Constraint == "max_logs_length" && pqErr.Table == "workspace_agents"
}
return false

View File

@ -0,0 +1,8 @@
BEGIN;
ALTER TABLE workspace_agent_logs RENAME TO workspace_agent_startup_logs;
ALTER TABLE workspace_agent_startup_logs DROP COLUMN source;
DROP TYPE workspace_agent_log_source;
ALTER TABLE workspace_agents RENAME COLUMN logs_overflowed TO startup_logs_overflowed;
ALTER TABLE workspace_agents RENAME COLUMN logs_length TO startup_logs_length;
ALTER TABLE workspace_agents RENAME CONSTRAINT max_logs_length TO max_startup_logs_length;
COMMIT;

View File

@ -0,0 +1,8 @@
BEGIN;
CREATE TYPE workspace_agent_log_source AS ENUM ('startup_script', 'shutdown_script', 'kubernetes_logs', 'envbox', 'envbuilder', 'external');
ALTER TABLE workspace_agent_startup_logs RENAME TO workspace_agent_logs;
ALTER TABLE workspace_agent_logs ADD COLUMN source workspace_agent_log_source NOT NULL DEFAULT 'startup_script';
ALTER TABLE workspace_agents RENAME COLUMN startup_logs_overflowed TO logs_overflowed;
ALTER TABLE workspace_agents RENAME COLUMN startup_logs_length TO logs_length;
ALTER TABLE workspace_agents RENAME CONSTRAINT max_startup_logs_length TO max_logs_length;
COMMIT;

View File

@ -1169,6 +1169,76 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState {
}
}
type WorkspaceAgentLogSource string
const (
WorkspaceAgentLogSourceStartupScript WorkspaceAgentLogSource = "startup_script"
WorkspaceAgentLogSourceShutdownScript WorkspaceAgentLogSource = "shutdown_script"
WorkspaceAgentLogSourceKubernetesLogs WorkspaceAgentLogSource = "kubernetes_logs"
WorkspaceAgentLogSourceEnvbox WorkspaceAgentLogSource = "envbox"
WorkspaceAgentLogSourceEnvbuilder WorkspaceAgentLogSource = "envbuilder"
WorkspaceAgentLogSourceExternal WorkspaceAgentLogSource = "external"
)
func (e *WorkspaceAgentLogSource) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = WorkspaceAgentLogSource(s)
case string:
*e = WorkspaceAgentLogSource(s)
default:
return fmt.Errorf("unsupported scan type for WorkspaceAgentLogSource: %T", src)
}
return nil
}
type NullWorkspaceAgentLogSource struct {
WorkspaceAgentLogSource WorkspaceAgentLogSource `json:"workspace_agent_log_source"`
Valid bool `json:"valid"` // Valid is true if WorkspaceAgentLogSource is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullWorkspaceAgentLogSource) Scan(value interface{}) error {
if value == nil {
ns.WorkspaceAgentLogSource, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.WorkspaceAgentLogSource.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullWorkspaceAgentLogSource) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.WorkspaceAgentLogSource), nil
}
func (e WorkspaceAgentLogSource) Valid() bool {
switch e {
case WorkspaceAgentLogSourceStartupScript,
WorkspaceAgentLogSourceShutdownScript,
WorkspaceAgentLogSourceKubernetesLogs,
WorkspaceAgentLogSourceEnvbox,
WorkspaceAgentLogSourceEnvbuilder,
WorkspaceAgentLogSourceExternal:
return true
}
return false
}
func AllWorkspaceAgentLogSourceValues() []WorkspaceAgentLogSource {
return []WorkspaceAgentLogSource{
WorkspaceAgentLogSourceStartupScript,
WorkspaceAgentLogSourceShutdownScript,
WorkspaceAgentLogSourceKubernetesLogs,
WorkspaceAgentLogSourceEnvbox,
WorkspaceAgentLogSourceEnvbuilder,
WorkspaceAgentLogSourceExternal,
}
}
type WorkspaceAgentSubsystem string
const (
@ -1806,10 +1876,10 @@ type WorkspaceAgent struct {
// The number of seconds to wait for the shutdown script to complete. If the script does not complete within this time, the agent lifecycle will be marked as shutdown_timeout.
ShutdownScriptTimeoutSeconds int32 `db:"shutdown_script_timeout_seconds" json:"shutdown_script_timeout_seconds"`
// Total length of startup logs
StartupLogsLength int32 `db:"startup_logs_length" json:"startup_logs_length"`
LogsLength int32 `db:"logs_length" json:"logs_length"`
// Whether the startup logs overflowed in length
StartupLogsOverflowed bool `db:"startup_logs_overflowed" json:"startup_logs_overflowed"`
Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"`
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"`
// When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible.
StartupScriptBehavior StartupScriptBehavior `db:"startup_script_behavior" json:"startup_script_behavior"`
// The time the agent entered the starting lifecycle state
@ -1818,6 +1888,15 @@ type WorkspaceAgent struct {
ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
}
type WorkspaceAgentLog struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output string `db:"output" json:"output"`
ID int64 `db:"id" json:"id"`
Level LogLevel `db:"level" json:"level"`
Source WorkspaceAgentLogSource `db:"source" json:"source"`
}
type WorkspaceAgentMetadatum struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
DisplayName string `db:"display_name" json:"display_name"`
@ -1830,14 +1909,6 @@ type WorkspaceAgentMetadatum struct {
CollectedAt time.Time `db:"collected_at" json:"collected_at"`
}
type WorkspaceAgentStartupLog struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output string `db:"output" json:"output"`
ID int64 `db:"id" json:"id"`
Level LogLevel `db:"level" json:"level"`
}
type WorkspaceAgentStat struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`

View File

@ -36,7 +36,7 @@ type sqlcQuerier interface {
DeleteLicense(ctx context.Context, id int32) (int32, error)
// If an agent hasn't connected in the last 7 days, we purge it's logs.
// Logs can take up a lot of space, so it's important we clean up frequently.
DeleteOldWorkspaceAgentStartupLogs(ctx context.Context) error
DeleteOldWorkspaceAgentLogs(ctx context.Context) error
DeleteOldWorkspaceAgentStats(ctx context.Context) error
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error)
@ -145,8 +145,8 @@ type sqlcQuerier interface {
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error)
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error)
GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg GetWorkspaceAgentStartupLogsAfterParams) ([]WorkspaceAgentStartupLog, error)
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error)
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
@ -215,8 +215,8 @@ type sqlcQuerier interface {
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
InsertWorkspaceAgentStartupLogs(ctx context.Context, arg InsertWorkspaceAgentStartupLogsParams) ([]WorkspaceAgentStartupLog, error)
InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error)
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error
@ -260,9 +260,9 @@ type sqlcQuerier interface {
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error)
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error
UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error
UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error
UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error
UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentStartupLogOverflowByIDParams) error
UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error

View File

@ -91,7 +91,7 @@ func TestGetDeploymentWorkspaceAgentStats(t *testing.T) {
})
}
func TestInsertWorkspaceAgentStartupLogs(t *testing.T) {
func TestInsertWorkspaceAgentLogs(t *testing.T) {
t.Parallel()
if testing.Short() {
t.SkipNow()
@ -111,25 +111,27 @@ func TestInsertWorkspaceAgentStartupLogs(t *testing.T) {
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
logs, err := db.InsertWorkspaceAgentStartupLogs(ctx, database.InsertWorkspaceAgentStartupLogsParams{
logs, err := db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: agent.ID,
CreatedAt: []time.Time{database.Now()},
Output: []string{"first"},
Level: []database.LogLevel{database.LogLevelInfo},
Source: []database.WorkspaceAgentLogSource{database.WorkspaceAgentLogSourceExternal},
// 1 MB is the max
OutputLength: 1 << 20,
})
require.NoError(t, err)
require.Equal(t, int64(1), logs[0].ID)
_, err = db.InsertWorkspaceAgentStartupLogs(ctx, database.InsertWorkspaceAgentStartupLogsParams{
_, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: agent.ID,
CreatedAt: []time.Time{database.Now()},
Output: []string{"second"},
Level: []database.LogLevel{database.LogLevelInfo},
Source: []database.WorkspaceAgentLogSource{database.WorkspaceAgentLogSourceExternal},
OutputLength: 1,
})
require.True(t, database.IsStartupLogsLimitError(err))
require.True(t, database.IsWorkspaceAgentLogsLimitError(err))
}
func TestProxyByHostname(t *testing.T) {

View File

@ -5914,22 +5914,22 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
return i, err
}
const deleteOldWorkspaceAgentStartupLogs = `-- name: DeleteOldWorkspaceAgentStartupLogs :exec
DELETE FROM workspace_agent_startup_logs WHERE agent_id IN
const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :exec
DELETE FROM workspace_agent_logs WHERE agent_id IN
(SELECT id FROM workspace_agents WHERE last_connected_at IS NOT NULL
AND last_connected_at < NOW() - INTERVAL '7 day')
`
// If an agent hasn't connected in the last 7 days, we purge it's logs.
// Logs can take up a lot of space, so it's important we clean up frequently.
func (q *sqlQuerier) DeleteOldWorkspaceAgentStartupLogs(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentStartupLogs)
func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentLogs)
return err
}
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
FROM
workspace_agents
WHERE
@ -5969,8 +5969,8 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -5981,7 +5981,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
FROM
workspace_agents
WHERE
@ -6019,8 +6019,8 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6031,7 +6031,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
FROM
workspace_agents
WHERE
@ -6071,8 +6071,8 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6105,6 +6105,53 @@ func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id
return i, err
}
const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many
SELECT
agent_id, created_at, output, id, level, source
FROM
workspace_agent_logs
WHERE
agent_id = $1
AND (
id > $2
) ORDER BY id ASC
`
type GetWorkspaceAgentLogsAfterParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAfter int64 `db:"created_after" json:"created_after"`
}
func (q *sqlQuerier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogsAfter, arg.AgentID, arg.CreatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLog
for rows.Next() {
var i WorkspaceAgentLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
&i.Source,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many
SELECT
workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at
@ -6147,55 +6194,9 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAge
return items, nil
}
const getWorkspaceAgentStartupLogsAfter = `-- name: GetWorkspaceAgentStartupLogsAfter :many
SELECT
agent_id, created_at, output, id, level
FROM
workspace_agent_startup_logs
WHERE
agent_id = $1
AND (
id > $2
) ORDER BY id ASC
`
type GetWorkspaceAgentStartupLogsAfterParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAfter int64 `db:"created_after" json:"created_after"`
}
func (q *sqlQuerier) GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg GetWorkspaceAgentStartupLogsAfterParams) ([]WorkspaceAgentStartupLog, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentStartupLogsAfter, arg.AgentID, arg.CreatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentStartupLog
for rows.Next() {
var i WorkspaceAgentStartupLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
FROM
workspace_agents
WHERE
@ -6239,8 +6240,8 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6260,7 +6261,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
}
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at FROM workspace_agents WHERE created_at > $1
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at FROM workspace_agents WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
@ -6300,8 +6301,8 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6322,7 +6323,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.startup_logs_length, workspace_agents.startup_logs_overflowed, workspace_agents.subsystem, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.subsystem, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at
FROM
workspace_agents
JOIN
@ -6378,8 +6379,8 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6424,7 +6425,7 @@ INSERT INTO
shutdown_script_timeout_seconds
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, startup_logs_length, startup_logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
`
type InsertWorkspaceAgentParams struct {
@ -6504,8 +6505,8 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
&i.ExpandedDirectory,
&i.ShutdownScript,
&i.ShutdownScriptTimeoutSeconds,
&i.StartupLogsLength,
&i.StartupLogsOverflowed,
&i.LogsLength,
&i.LogsOverflowed,
&i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
@ -6514,6 +6515,68 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
return i, err
}
const insertWorkspaceAgentLogs = `-- name: InsertWorkspaceAgentLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
logs_length = logs_length + $6 WHERE workspace_agents.id = $1
)
INSERT INTO
workspace_agent_logs (agent_id, created_at, output, level, source)
SELECT
$1 :: uuid AS agent_id,
unnest($2 :: timestamptz [ ]) AS created_at,
unnest($3 :: VARCHAR(1024) [ ]) AS output,
unnest($4 :: log_level [ ]) AS level,
unnest($5 :: workspace_agent_log_source [ ]) AS source
RETURNING workspace_agent_logs.agent_id, workspace_agent_logs.created_at, workspace_agent_logs.output, workspace_agent_logs.id, workspace_agent_logs.level, workspace_agent_logs.source
`
type InsertWorkspaceAgentLogsParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
Output []string `db:"output" json:"output"`
Level []LogLevel `db:"level" json:"level"`
Source []WorkspaceAgentLogSource `db:"source" json:"source"`
OutputLength int32 `db:"output_length" json:"output_length"`
}
func (q *sqlQuerier) InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogs,
arg.AgentID,
pq.Array(arg.CreatedAt),
pq.Array(arg.Output),
pq.Array(arg.Level),
pq.Array(arg.Source),
arg.OutputLength,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLog
for rows.Next() {
var i WorkspaceAgentLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
&i.Source,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec
INSERT INTO
workspace_agent_metadata (
@ -6549,64 +6612,6 @@ func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg Inser
return err
}
const insertWorkspaceAgentStartupLogs = `-- name: InsertWorkspaceAgentStartupLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
startup_logs_length = startup_logs_length + $5 WHERE workspace_agents.id = $1
)
INSERT INTO
workspace_agent_startup_logs (agent_id, created_at, output, level)
SELECT
$1 :: uuid AS agent_id,
unnest($2 :: timestamptz [ ]) AS created_at,
unnest($3 :: VARCHAR(1024) [ ]) AS output,
unnest($4 :: log_level [ ]) AS level
RETURNING workspace_agent_startup_logs.agent_id, workspace_agent_startup_logs.created_at, workspace_agent_startup_logs.output, workspace_agent_startup_logs.id, workspace_agent_startup_logs.level
`
type InsertWorkspaceAgentStartupLogsParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
Output []string `db:"output" json:"output"`
Level []LogLevel `db:"level" json:"level"`
OutputLength int32 `db:"output_length" json:"output_length"`
}
func (q *sqlQuerier) InsertWorkspaceAgentStartupLogs(ctx context.Context, arg InsertWorkspaceAgentStartupLogsParams) ([]WorkspaceAgentStartupLog, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentStartupLogs,
arg.AgentID,
pq.Array(arg.CreatedAt),
pq.Array(arg.Output),
pq.Array(arg.Level),
arg.OutputLength,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentStartupLog
for rows.Next() {
var i WorkspaceAgentStartupLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec
UPDATE
workspace_agents
@ -6669,6 +6674,25 @@ func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context,
return err
}
const updateWorkspaceAgentLogOverflowByID = `-- name: UpdateWorkspaceAgentLogOverflowByID :exec
UPDATE
workspace_agents
SET
logs_overflowed = $2
WHERE
id = $1
`
type UpdateWorkspaceAgentLogOverflowByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLogOverflowByID, arg.ID, arg.LogsOverflowed)
return err
}
const updateWorkspaceAgentMetadata = `-- name: UpdateWorkspaceAgentMetadata :exec
UPDATE
workspace_agent_metadata
@ -6728,25 +6752,6 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up
return err
}
const updateWorkspaceAgentStartupLogOverflowByID = `-- name: UpdateWorkspaceAgentStartupLogOverflowByID :exec
UPDATE
workspace_agents
SET
startup_logs_overflowed = $2
WHERE
id = $1
`
type UpdateWorkspaceAgentStartupLogOverflowByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
StartupLogsOverflowed bool `db:"startup_logs_overflowed" json:"startup_logs_overflowed"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentStartupLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentStartupLogOverflowByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentStartupLogOverflowByID, arg.ID, arg.StartupLogsOverflowed)
return err
}
const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'
`

View File

@ -140,43 +140,44 @@ FROM
WHERE
workspace_agent_id = $1;
-- name: UpdateWorkspaceAgentStartupLogOverflowByID :exec
-- name: UpdateWorkspaceAgentLogOverflowByID :exec
UPDATE
workspace_agents
SET
startup_logs_overflowed = $2
logs_overflowed = $2
WHERE
id = $1;
-- name: GetWorkspaceAgentStartupLogsAfter :many
-- name: GetWorkspaceAgentLogsAfter :many
SELECT
*
FROM
workspace_agent_startup_logs
workspace_agent_logs
WHERE
agent_id = $1
AND (
id > @created_after
) ORDER BY id ASC;
-- name: InsertWorkspaceAgentStartupLogs :many
-- name: InsertWorkspaceAgentLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
startup_logs_length = startup_logs_length + @output_length WHERE workspace_agents.id = @agent_id
logs_length = logs_length + @output_length WHERE workspace_agents.id = @agent_id
)
INSERT INTO
workspace_agent_startup_logs (agent_id, created_at, output, level)
workspace_agent_logs (agent_id, created_at, output, level, source)
SELECT
@agent_id :: uuid AS agent_id,
unnest(@created_at :: timestamptz [ ]) AS created_at,
unnest(@output :: VARCHAR(1024) [ ]) AS output,
unnest(@level :: log_level [ ]) AS level
RETURNING workspace_agent_startup_logs.*;
unnest(@level :: log_level [ ]) AS level,
unnest(@source :: workspace_agent_log_source [ ]) AS source
RETURNING workspace_agent_logs.*;
-- If an agent hasn't connected in the last 7 days, we purge it's logs.
-- Logs can take up a lot of space, so it's important we clean up frequently.
-- name: DeleteOldWorkspaceAgentStartupLogs :exec
DELETE FROM workspace_agent_startup_logs WHERE agent_id IN
-- name: DeleteOldWorkspaceAgentLogs :exec
DELETE FROM workspace_agent_logs WHERE agent_id IN
(SELECT id FROM workspace_agents WHERE last_connected_at IS NOT NULL
AND last_connected_at < NOW() - INTERVAL '7 day');

View File

@ -27,3 +27,32 @@ func templateVersionParametersDeprecated(rw http.ResponseWriter, r *http.Request
func templateVersionSchemaDeprecated(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, []struct{}{})
}
// @Summary Removed: Patch workspace agent logs
// @ID removed-patch-workspace-agent-logs
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body agentsdk.PatchLogs true "logs"
// @Success 200 {object} codersdk.Response
// @Router /workspaceagents/me/startup-logs [patch]
func (api *API) patchWorkspaceAgentLogsDeprecated(rw http.ResponseWriter, r *http.Request) {
api.patchWorkspaceAgentLogs(rw, r)
}
// @Summary Removed: Get logs by workspace agent
// @ID removed-get-logs-by-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Param before query int false "Before log id"
// @Param after query int false "After log id"
// @Param follow query bool false "Follow log stream"
// @Param no_compression query bool false "Disable compression for WebSocket connection"
// @Success 200 {array} codersdk.WorkspaceAgentLog
// @Router /workspaceagents/{workspaceagent}/startup-logs [get]
func (api *API) workspaceAgentLogsDeprecated(rw http.ResponseWriter, r *http.Request) {
api.workspaceAgentLogs(rw, r)
}

View File

@ -234,20 +234,20 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques
httpapi.Write(ctx, rw, http.StatusOK, nil)
}
// @Summary Patch workspace agent startup logs
// @ID patch-workspace-agent-startup-logs
// @Summary Patch workspace agent logs
// @ID patch-workspace-agent-logs
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body agentsdk.PatchStartupLogs true "Startup logs"
// @Param request body agentsdk.PatchLogs true "logs"
// @Success 200 {object} codersdk.Response
// @Router /workspaceagents/me/startup-logs [patch]
func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Request) {
// @Router /workspaceagents/me/logs [patch]
func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
var req agentsdk.PatchStartupLogs
var req agentsdk.PatchLogs
if !httpapi.Read(ctx, rw, r, &req) {
return
}
@ -260,6 +260,7 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
createdAt := make([]time.Time, 0)
output := make([]string, 0)
level := make([]database.LogLevel, 0)
source := make([]database.WorkspaceAgentLogSource, 0)
outputLength := 0
for _, logEntry := range req.Logs {
createdAt = append(createdAt, logEntry.CreatedAt)
@ -278,39 +279,54 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
return
}
level = append(level, parsedLevel)
if logEntry.Source == "" {
// Default to "startup_script" to support older agents that didn't have the source field.
logEntry.Source = codersdk.WorkspaceAgentLogSourceStartupScript
}
parsedSource := database.WorkspaceAgentLogSource(logEntry.Source)
if !parsedSource.Valid() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid log source provided.",
Detail: fmt.Sprintf("invalid log source: %q", logEntry.Source),
})
return
}
source = append(source, parsedSource)
}
logs, err := api.Database.InsertWorkspaceAgentStartupLogs(ctx, database.InsertWorkspaceAgentStartupLogsParams{
logs, err := api.Database.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{
AgentID: workspaceAgent.ID,
CreatedAt: createdAt,
Output: output,
Level: level,
Source: source,
OutputLength: int32(outputLength),
})
if err != nil {
if !database.IsStartupLogsLimitError(err) {
if !database.IsWorkspaceAgentLogsLimitError(err) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to upload startup logs",
Message: "Failed to upload logs",
Detail: err.Error(),
})
return
}
if workspaceAgent.StartupLogsOverflowed {
if workspaceAgent.LogsOverflowed {
httpapi.Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{
Message: "Startup logs limit exceeded",
Message: "Logs limit exceeded",
Detail: err.Error(),
})
return
}
err := api.Database.UpdateWorkspaceAgentStartupLogOverflowByID(ctx, database.UpdateWorkspaceAgentStartupLogOverflowByIDParams{
ID: workspaceAgent.ID,
StartupLogsOverflowed: true,
err := api.Database.UpdateWorkspaceAgentLogOverflowByID(ctx, database.UpdateWorkspaceAgentLogOverflowByIDParams{
ID: workspaceAgent.ID,
LogsOverflowed: true,
})
if err != nil {
// We don't want to return here, because the agent will retry
// on failure and this isn't a huge deal. The overflow state
// is just a hint to the user that the logs are incomplete.
api.Logger.Warn(ctx, "failed to update workspace agent startup log overflow", slog.Error(err))
api.Logger.Warn(ctx, "failed to update workspace agent log overflow", slog.Error(err))
}
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
@ -334,7 +350,7 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
api.publishWorkspaceUpdate(ctx, build.WorkspaceID)
httpapi.Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{
Message: "Startup logs limit exceeded",
Message: "Logs limit exceeded",
})
return
}
@ -343,11 +359,11 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
// Publish by the lowest log ID inserted so the
// log stream will fetch everything from that point.
api.publishWorkspaceAgentStartupLogsUpdate(ctx, workspaceAgent.ID, agentsdk.StartupLogsNotifyMessage{
api.publishWorkspaceAgentLogsUpdate(ctx, workspaceAgent.ID, agentsdk.LogsNotifyMessage{
CreatedAfter: lowestLogID - 1,
})
if workspaceAgent.StartupLogsLength == 0 {
if workspaceAgent.LogsLength == 0 {
// If these are the first logs being appended, we publish a UI update
// to notify the UI that logs are now available.
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
@ -374,11 +390,10 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
httpapi.Write(ctx, rw, http.StatusOK, nil)
}
// workspaceAgentStartupLogs returns the logs sent from a workspace agent
// during startup.
// workspaceAgentLogs returns the logs associated with a workspace agent
//
// @Summary Get startup logs by workspace agent
// @ID get-startup-logs-by-workspace-agent
// @Summary Get logs by workspace agent
// @ID get-logs-by-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
@ -387,9 +402,9 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
// @Param after query int false "After log id"
// @Param follow query bool false "Follow log stream"
// @Param no_compression query bool false "Disable compression for WebSocket connection"
// @Success 200 {array} codersdk.WorkspaceAgentStartupLog
// @Router /workspaceagents/{workspaceagent}/startup-logs [get]
func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {array} codersdk.WorkspaceAgentLog
// @Router /workspaceagents/{workspaceagent}/logs [get]
func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
// This mostly copies how provisioner job logs are streamed!
var (
ctx = r.Context()
@ -416,7 +431,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
}
}
logs, err := api.Database.GetWorkspaceAgentStartupLogsAfter(ctx, database.GetWorkspaceAgentStartupLogsAfterParams{
logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{
AgentID: workspaceAgent.ID,
CreatedAfter: after,
})
@ -431,11 +446,11 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
return
}
if logs == nil {
logs = []database.WorkspaceAgentStartupLog{}
logs = []database.WorkspaceAgentLog{}
}
if !follow {
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspaceAgentStartupLogs(logs))
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspaceAgentLogs(logs))
return
}
@ -472,7 +487,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
// The Go stdlib JSON encoder appends a newline character after message write.
encoder := json.NewEncoder(wsNetConn)
err = encoder.Encode(convertWorkspaceAgentStartupLogs(logs))
err = encoder.Encode(convertWorkspaceAgentLogs(logs))
if err != nil {
return
}
@ -488,7 +503,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
notifyCh <- struct{}{}
// Subscribe early to prevent missing log events.
closeSubscribe, err := api.Pubsub.Subscribe(agentsdk.StartupLogsNotifyChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) {
closeSubscribe, err := api.Pubsub.Subscribe(agentsdk.LogsNotifyChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) {
// The message is not important, we're tracking lastSentLogID manually.
select {
case notifyCh <- struct{}{}:
@ -497,7 +512,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to subscribe to startup logs.",
Message: "Failed to subscribe to logs.",
Detail: err.Error(),
})
return
@ -505,7 +520,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
defer closeSubscribe()
// Buffer size controls the log prefetch capacity.
bufferedLogs := make(chan []database.WorkspaceAgentStartupLog, 8)
bufferedLogs := make(chan []database.WorkspaceAgentLog, 8)
// Check at least once per minute in case we didn't receive a pubsub message.
recheckInterval := time.Minute
t := time.NewTicker(recheckInterval)
@ -523,7 +538,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
t.Reset(recheckInterval)
}
logs, err := api.Database.GetWorkspaceAgentStartupLogsAfter(ctx, database.GetWorkspaceAgentStartupLogsAfterParams{
logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{
AgentID: workspaceAgent.ID,
CreatedAfter: lastSentLogID,
})
@ -531,7 +546,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
if xerrors.Is(err, context.Canceled) {
return
}
logger.Warn(ctx, "failed to get workspace agent startup logs after", slog.Error(err))
logger.Warn(ctx, "failed to get workspace agent logs after", slog.Error(err))
continue
}
if len(logs) == 0 {
@ -569,7 +584,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
}
return
}
err = encoder.Encode(convertWorkspaceAgentStartupLogs(logs))
err = encoder.Encode(convertWorkspaceAgentLogs(logs))
if err != nil {
return
}
@ -1244,8 +1259,8 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
StartupScript: dbAgent.StartupScript.String,
StartupScriptBehavior: codersdk.WorkspaceAgentStartupScriptBehavior(dbAgent.StartupScriptBehavior),
StartupScriptTimeoutSeconds: dbAgent.StartupScriptTimeoutSeconds,
StartupLogsLength: dbAgent.StartupLogsLength,
StartupLogsOverflowed: dbAgent.StartupLogsOverflowed,
LogsLength: dbAgent.LogsLength,
LogsOverflowed: dbAgent.LogsOverflowed,
Version: dbAgent.Version,
EnvironmentVariables: envs,
Directory: dbAgent.Directory,
@ -2076,16 +2091,16 @@ func websocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websock
}
}
func convertWorkspaceAgentStartupLogs(logs []database.WorkspaceAgentStartupLog) []codersdk.WorkspaceAgentStartupLog {
sdk := make([]codersdk.WorkspaceAgentStartupLog, 0, len(logs))
func convertWorkspaceAgentLogs(logs []database.WorkspaceAgentLog) []codersdk.WorkspaceAgentLog {
sdk := make([]codersdk.WorkspaceAgentLog, 0, len(logs))
for _, logEntry := range logs {
sdk = append(sdk, convertWorkspaceAgentStartupLog(logEntry))
sdk = append(sdk, convertWorkspaceAgentLog(logEntry))
}
return sdk
}
func convertWorkspaceAgentStartupLog(logEntry database.WorkspaceAgentStartupLog) codersdk.WorkspaceAgentStartupLog {
return codersdk.WorkspaceAgentStartupLog{
func convertWorkspaceAgentLog(logEntry database.WorkspaceAgentLog) codersdk.WorkspaceAgentLog {
return codersdk.WorkspaceAgentLog{
ID: logEntry.ID,
CreatedAt: logEntry.CreatedAt,
Output: logEntry.Output,

View File

@ -213,8 +213,8 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
err := agentClient.PatchStartupLogs(ctx, agentsdk.PatchStartupLogs{
Logs: []agentsdk.StartupLog{
err := agentClient.PatchLogs(ctx, agentsdk.PatchLogs{
Logs: []agentsdk.Log{
{
CreatedAt: database.Now(),
Output: "testing",
@ -227,12 +227,12 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
})
require.NoError(t, err)
logs, closer, err := client.WorkspaceAgentStartupLogsAfter(ctx, build.Resources[0].Agents[0].ID, 0, true)
logs, closer, err := client.WorkspaceAgentLogsAfter(ctx, build.Resources[0].Agents[0].ID, 0, true)
require.NoError(t, err)
defer func() {
_ = closer.Close()
}()
var logChunk []codersdk.WorkspaceAgentStartupLog
var logChunk []codersdk.WorkspaceAgentLog
select {
case <-ctx.Done():
case logChunk = <-logs:
@ -280,8 +280,8 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
err = agentClient.PatchStartupLogs(ctx, agentsdk.PatchStartupLogs{
Logs: []agentsdk.StartupLog{{
err = agentClient.PatchLogs(ctx, agentsdk.PatchLogs{
Logs: []agentsdk.Log{{
CreatedAt: database.Now(),
Output: strings.Repeat("a", (1<<20)+1),
}},
@ -299,7 +299,7 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
t.FailNow()
case update = <-updates:
}
if update.LatestBuild.Resources[0].Agents[0].StartupLogsOverflowed {
if update.LatestBuild.Resources[0].Agents[0].LogsOverflowed {
break
}
}

View File

@ -1251,13 +1251,13 @@ func (api *API) publishWorkspaceUpdate(ctx context.Context, workspaceID uuid.UUI
}
}
func (api *API) publishWorkspaceAgentStartupLogsUpdate(ctx context.Context, workspaceAgentID uuid.UUID, m agentsdk.StartupLogsNotifyMessage) {
func (api *API) publishWorkspaceAgentLogsUpdate(ctx context.Context, workspaceAgentID uuid.UUID, m agentsdk.LogsNotifyMessage) {
b, err := json.Marshal(m)
if err != nil {
api.Logger.Warn(ctx, "failed to marshal startup logs notify message", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
api.Logger.Warn(ctx, "failed to marshal logs notify message", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
}
err = api.Pubsub.Publish(agentsdk.StartupLogsNotifyChannel(workspaceAgentID), b)
err = api.Pubsub.Publish(agentsdk.LogsNotifyChannel(workspaceAgentID), b)
if err != nil {
api.Logger.Warn(ctx, "failed to publish workspace agent startup logs update", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
api.Logger.Warn(ctx, "failed to publish workspace agent logs update", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
}
}

View File

@ -274,7 +274,7 @@ func (*client) PostStartup(_ context.Context, _ agentsdk.PostStartupRequest) err
return nil
}
func (*client) PatchStartupLogs(_ context.Context, _ agentsdk.PatchStartupLogs) error {
func (*client) PatchLogs(_ context.Context, _ agentsdk.PatchLogs) error {
return nil
}

View File

@ -621,20 +621,21 @@ 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"`
Level codersdk.LogLevel `json:"level"`
type Log struct {
CreatedAt time.Time `json:"created_at"`
Output string `json:"output"`
Level codersdk.LogLevel `json:"level"`
Source codersdk.WorkspaceAgentLogSource `json:"source"`
}
type PatchStartupLogs struct {
Logs []StartupLog `json:"logs"`
type PatchLogs struct {
Logs []Log `json:"logs"`
}
// PatchStartupLogs writes log messages to the agent startup script.
// PatchLogs 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)
func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error {
res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/logs", req)
if err != nil {
return err
}
@ -737,13 +738,13 @@ func websocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websock
}
}
// 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)
// LogsNotifyChannel returns the channel name responsible for notifying
// of new logs.
func LogsNotifyChannel(agentID uuid.UUID) string {
return fmt.Sprintf("agent-logs:%s", agentID)
}
type StartupLogsNotifyMessage struct {
type LogsNotifyMessage struct {
CreatedAfter int64 `json:"created_after"`
}

View File

@ -16,10 +16,11 @@ import (
)
type startupLogsWriter struct {
buf bytes.Buffer // Buffer to track partial lines.
ctx context.Context
send func(ctx context.Context, log ...StartupLog) error
level codersdk.LogLevel
buf bytes.Buffer // Buffer to track partial lines.
ctx context.Context
send func(ctx context.Context, log ...Log) error
level codersdk.LogLevel
source codersdk.WorkspaceAgentLogSource
}
func (w *startupLogsWriter) Write(p []byte) (int, error) {
@ -39,10 +40,11 @@ func (w *startupLogsWriter) Write(p []byte) (int, error) {
partial = w.buf.Bytes()
w.buf.Reset()
}
err := w.send(w.ctx, StartupLog{
err := w.send(w.ctx, Log{
CreatedAt: time.Now().UTC(), // UTC, like database.Now().
Level: w.level,
Output: string(partial) + string(p[:nl-cr]),
Source: w.source,
})
if err != nil {
return n - len(p), err
@ -61,10 +63,11 @@ func (w *startupLogsWriter) Write(p []byte) (int, error) {
func (w *startupLogsWriter) Close() error {
if w.buf.Len() > 0 {
defer w.buf.Reset()
return w.send(w.ctx, StartupLog{
return w.send(w.ctx, Log{
CreatedAt: time.Now().UTC(), // UTC, like database.Now().
Level: w.level,
Output: w.buf.String(),
Source: w.source,
})
}
return nil
@ -78,20 +81,24 @@ func (w *startupLogsWriter) Close() error {
//
// Neither Write nor Close is safe for concurrent use and must be used
// by a single goroutine.
func StartupLogsWriter(ctx context.Context, sender func(ctx context.Context, log ...StartupLog) error, level codersdk.LogLevel) io.WriteCloser {
func StartupLogsWriter(ctx context.Context, sender func(ctx context.Context, log ...Log) error, source codersdk.WorkspaceAgentLogSource, level codersdk.LogLevel) io.WriteCloser {
if source == "" {
source = codersdk.WorkspaceAgentLogSourceExternal
}
return &startupLogsWriter{
ctx: ctx,
send: sender,
level: level,
ctx: ctx,
send: sender,
level: level,
source: source,
}
}
// SendStartupLogs will send agent startup logs to the server. Calls to
// LogsSender will send agent startup logs to the server. Calls to
// sendLog are non-blocking and will return an error if flushAndClose
// has been called. Calling sendLog concurrently is not supported. If
// the context passed to flushAndClose is canceled, any remaining logs
// will be discarded.
func StartupLogsSender(patchStartupLogs func(ctx context.Context, req PatchStartupLogs) error, logger slog.Logger) (sendLog func(ctx context.Context, log ...StartupLog) error, flushAndClose func(context.Context) error) {
func LogsSender(patchLogs func(ctx context.Context, req PatchLogs) error, logger slog.Logger) (sendLog func(ctx context.Context, log ...Log) error, flushAndClose func(context.Context) error) {
// The main context is used to close the sender goroutine and cancel
// any outbound requests to the API. The shutdown context is used to
// signal the sender goroutine to flush logs and then exit.
@ -100,7 +107,7 @@ func StartupLogsSender(patchStartupLogs func(ctx context.Context, req PatchStart
// Synchronous sender, there can only be one outbound send at a time.
sendDone := make(chan struct{})
send := make(chan []StartupLog, 1)
send := make(chan []Log, 1)
go func() {
// Set flushTimeout and backlogLimit so that logs are uploaded
// once every 250ms or when 100 logs have been added to the
@ -110,7 +117,7 @@ func StartupLogsSender(patchStartupLogs func(ctx context.Context, req PatchStart
flush := time.NewTicker(flushTimeout)
var backlog []StartupLog
var backlog []Log
defer func() {
flush.Stop()
if len(backlog) > 0 {
@ -150,7 +157,7 @@ func StartupLogsSender(patchStartupLogs func(ctx context.Context, req PatchStart
// meaning these requests won't be interrupted by
// shutdown.
for r := retry.New(time.Second, 5*time.Second); r.Wait(ctx); {
err := patchStartupLogs(ctx, PatchStartupLogs{
err := patchLogs(ctx, PatchLogs{
Logs: backlog,
})
if err == nil {
@ -185,8 +192,8 @@ func StartupLogsSender(patchStartupLogs func(ctx context.Context, req PatchStart
}
}()
var queue []StartupLog
sendLog = func(callCtx context.Context, log ...StartupLog) error {
var queue []Log
sendLog = func(callCtx context.Context, log ...Log) error {
select {
case <-shutdownCtx.Done():
return xerrors.Errorf("closed: %w", shutdownCtx.Err())

View File

@ -28,8 +28,9 @@ func TestStartupLogsWriter_Write(t *testing.T) {
name string
ctx context.Context
level codersdk.LogLevel
source codersdk.WorkspaceAgentLogSource
writes []string
want []agentsdk.StartupLog
want []agentsdk.Log
wantErr bool
closeFirst bool
}{
@ -38,10 +39,12 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n"},
want: []agentsdk.StartupLog{
source: codersdk.WorkspaceAgentLogSourceShutdownScript,
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceShutdownScript,
},
},
},
@ -50,14 +53,16 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n", "goodbye world\n"},
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -66,30 +71,36 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"\n\n", "hello world\n\n\n", "goodbye world\n"},
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -98,10 +109,11 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n", "goodbye world"},
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -111,14 +123,16 @@ func TestStartupLogsWriter_Write(t *testing.T) {
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n", "goodbye world"},
closeFirst: true,
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -127,14 +141,16 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\n", "goodbye", " world\n"},
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -143,18 +159,21 @@ func TestStartupLogsWriter_Write(t *testing.T) {
ctx: context.Background(),
level: codersdk.LogLevelInfo,
writes: []string{"hello world\r\n", "\r\r\n", "goodbye world\n"},
want: []agentsdk.StartupLog{
want: []agentsdk.Log{
{
Level: codersdk.LogLevelInfo,
Output: "hello world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "\r",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
{
Level: codersdk.LogLevelInfo,
Output: "goodbye world",
Source: codersdk.WorkspaceAgentLogSourceExternal,
},
},
},
@ -172,8 +191,8 @@ func TestStartupLogsWriter_Write(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var got []agentsdk.StartupLog
send := func(ctx context.Context, log ...agentsdk.StartupLog) error {
var got []agentsdk.Log
send := func(ctx context.Context, log ...agentsdk.Log) error {
select {
case <-ctx.Done():
return ctx.Err()
@ -182,7 +201,7 @@ func TestStartupLogsWriter_Write(t *testing.T) {
got = append(got, log...)
return nil
}
w := agentsdk.StartupLogsWriter(tt.ctx, send, tt.level)
w := agentsdk.StartupLogsWriter(tt.ctx, send, tt.source, tt.level)
for _, s := range tt.writes {
_, err := w.Write([]byte(s))
if err != nil {
@ -233,7 +252,7 @@ func TestStartupLogsSender(t *testing.T) {
name string
sendCount int
discard []int
patchResp func(req agentsdk.PatchStartupLogs) error
patchResp func(req agentsdk.PatchLogs) error
}{
{
name: "single log",
@ -247,7 +266,7 @@ func TestStartupLogsSender(t *testing.T) {
name: "too large",
sendCount: 1,
discard: []int{1},
patchResp: func(req agentsdk.PatchStartupLogs) error {
patchResp: func(req agentsdk.PatchLogs) error {
return statusError(http.StatusRequestEntityTooLarge)
},
},
@ -260,8 +279,8 @@ func TestStartupLogsSender(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
got := []agentsdk.StartupLog{}
patchStartupLogs := func(_ context.Context, req agentsdk.PatchStartupLogs) error {
got := []agentsdk.Log{}
patchLogs := func(_ context.Context, req agentsdk.PatchLogs) error {
if tt.patchResp != nil {
err := tt.patchResp(req)
if err != nil {
@ -272,15 +291,15 @@ func TestStartupLogsSender(t *testing.T) {
return nil
}
sendLog, flushAndClose := agentsdk.StartupLogsSender(patchStartupLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
err := flushAndClose(ctx)
require.NoError(t, err)
}()
var want []agentsdk.StartupLog
var want []agentsdk.Log
for i := 0; i < tt.sendCount; i++ {
want = append(want, agentsdk.StartupLog{
want = append(want, agentsdk.Log{
CreatedAt: time.Now(),
Level: codersdk.LogLevelInfo,
Output: fmt.Sprintf("hello world %d", i),
@ -306,18 +325,18 @@ func TestStartupLogsSender(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
patchStartupLogs := func(_ context.Context, _ agentsdk.PatchStartupLogs) error {
patchLogs := func(_ context.Context, _ agentsdk.PatchLogs) error {
assert.Fail(t, "should not be called")
return nil
}
sendLog, flushAndClose := agentsdk.StartupLogsSender(patchStartupLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
_ = flushAndClose(ctx)
}()
cancel()
err := sendLog(ctx, agentsdk.StartupLog{
err := sendLog(ctx, agentsdk.Log{
CreatedAt: time.Now(),
Level: codersdk.LogLevelInfo,
Output: "hello world",
@ -336,18 +355,18 @@ func TestStartupLogsSender(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
var want, got []agentsdk.StartupLog
patchStartupLogs := func(_ context.Context, req agentsdk.PatchStartupLogs) error {
var want, got []agentsdk.Log
patchLogs := func(_ context.Context, req agentsdk.PatchLogs) error {
got = append(got, req.Logs...)
return nil
}
sendLog, flushAndClose := agentsdk.StartupLogsSender(patchStartupLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
defer func() {
_ = flushAndClose(ctx)
}()
err := sendLog(ctx, agentsdk.StartupLog{
err := sendLog(ctx, agentsdk.Log{
CreatedAt: time.Now(),
Level: codersdk.LogLevelInfo,
Output: "hello world",

View File

@ -153,8 +153,8 @@ type WorkspaceAgent struct {
StartupScript string `json:"startup_script,omitempty"`
StartupScriptBehavior WorkspaceAgentStartupScriptBehavior `json:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `json:"startup_script_timeout_seconds"` // StartupScriptTimeoutSeconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout.
StartupLogsLength int32 `json:"startup_logs_length"`
StartupLogsOverflowed bool `json:"startup_logs_overflowed"`
LogsLength int32 `json:"logs_length"`
LogsOverflowed bool `json:"logs_overflowed"`
Directory string `json:"directory,omitempty"`
ExpandedDirectory string `json:"expanded_directory,omitempty"`
Version string `json:"version"`
@ -628,7 +628,7 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.
}
//nolint:revive // Follow is a control flag on the server as well.
func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []WorkspaceAgentStartupLog, io.Closer, error) {
func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []WorkspaceAgentLog, io.Closer, error) {
var queryParams []string
if after != 0 {
queryParams = append(queryParams, fmt.Sprintf("after=%d", after))
@ -640,7 +640,7 @@ func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uui
if len(queryParams) > 0 {
query = "?" + strings.Join(queryParams, "&")
}
reqURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/startup-logs%s", agentID, query))
reqURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/logs%s", agentID, query))
if err != nil {
return nil, nil, err
}
@ -656,13 +656,13 @@ func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uui
return nil, nil, ReadBodyAsError(resp)
}
var logs []WorkspaceAgentStartupLog
var logs []WorkspaceAgentLog
err = json.NewDecoder(resp.Body).Decode(&logs)
if err != nil {
return nil, nil, xerrors.Errorf("decode startup logs: %w", err)
}
ch := make(chan []WorkspaceAgentStartupLog, 1)
ch := make(chan []WorkspaceAgentLog, 1)
ch <- logs
close(ch)
return ch, closeFunc(func() error { return nil }), nil
@ -690,7 +690,7 @@ func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uui
}
return nil, nil, ReadBodyAsError(res)
}
logChunks := make(chan []WorkspaceAgentStartupLog)
logChunks := make(chan []WorkspaceAgentLog)
closed := make(chan struct{})
ctx, wsNetConn := websocketNetConn(ctx, conn, websocket.MessageText)
decoder := json.NewDecoder(wsNetConn)
@ -699,7 +699,7 @@ func (c *Client) WorkspaceAgentStartupLogsAfter(ctx context.Context, agentID uui
defer close(logChunks)
defer conn.Close(websocket.StatusGoingAway, "")
for {
var logs []WorkspaceAgentStartupLog
var logs []WorkspaceAgentLog
err = decoder.Decode(&logs)
if err != nil {
return
@ -744,7 +744,7 @@ const (
GitProviderBitBucket GitProvider = "bitbucket"
)
type WorkspaceAgentStartupLog struct {
type WorkspaceAgentLog struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Output string `json:"output"`
@ -756,3 +756,14 @@ type AgentSubsystem string
const (
AgentSubsystemEnvbox AgentSubsystem = "envbox"
)
type WorkspaceAgentLogSource string
const (
WorkspaceAgentLogSourceStartupScript WorkspaceAgentLogSource = "startup_script"
WorkspaceAgentLogSourceShutdownScript WorkspaceAgentLogSource = "shutdown_script"
WorkspaceAgentLogSourceKubernetes WorkspaceAgentLogSource = "kubernetes"
WorkspaceAgentLogSourceEnvbox WorkspaceAgentLogSource = "envbox"
WorkspaceAgentLogSourceEnvbuilder WorkspaceAgentLogSource = "envbuilder"
WorkspaceAgentLogSourceExternal WorkspaceAgentLogSource = "external"
)

View File

@ -44,10 +44,10 @@ We strive to keep the following use cases up to date, but please note that chang
Workspace agents have a special token that can send logs, metrics, and workspace activity.
- [Custom workspace logs](../api/agents.md#patch-workspace-agent-startup-logs): Expose messages prior to the Coder init script running (e.g. pulling image, VM starting, restoring snapshot). [coder-logstream-kube](https://github.com/coder/coder-logstream-kube) uses this to show Kubernetes events, such as image pulls or ResourceQuota restrictions.
- [Custom workspace logs](../api/agents.md#patch-workspace-agent-logs): Expose messages prior to the Coder init script running (e.g. pulling image, VM starting, restoring snapshot). [coder-logstream-kube](https://github.com/coder/coder-logstream-kube) uses this to show Kubernetes events, such as image pulls or ResourceQuota restrictions.
```sh
curl -X PATCH https://coder.example.com/api/v2/workspaceagents/me/startup-logs \
curl -X PATCH https://coder.example.com/api/v2/workspaceagents/me/logs \
-H "Coder-Session-Token: $CODER_AGENT_TOKEN" \
-d "{
\"logs\": [

153
docs/api/agents.md generated
View File

@ -293,6 +293,66 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/gitsshkey \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Patch workspace agent logs
### Code samples
```shell
# Example request using curl
curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/logs \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`PATCH /workspaceagents/me/logs`
> Body parameter
```json
{
"logs": [
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
}
]
}
```
### Parameters
| Name | In | Type | Required | Description |
| ------ | ---- | -------------------------------------------------- | -------- | ----------- |
| `body` | body | [agentsdk.PatchLogs](schemas.md#agentsdkpatchlogs) | true | logs |
### Example responses
> 200 Response
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get authorized workspace agent manifest
### Code samples
@ -488,7 +548,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/me/report-stats \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Patch workspace agent startup logs
## Removed: Patch workspace agent logs
### Code samples
@ -510,7 +570,8 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \
{
"created_at": "string",
"level": "trace",
"output": "string"
"output": "string",
"source": "startup_script"
}
]
}
@ -518,9 +579,9 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \
### Parameters
| Name | In | Type | Required | Description |
| ------ | ---- | ---------------------------------------------------------------- | -------- | ------------ |
| `body` | body | [agentsdk.PatchStartupLogs](schemas.md#agentsdkpatchstartuplogs) | true | Startup logs |
| Name | In | Type | Required | Description |
| ------ | ---- | -------------------------------------------------- | -------- | ----------- |
| `body` | body | [agentsdk.PatchLogs](schemas.md#agentsdkpatchlogs) | true | logs |
### Example responses
@ -621,6 +682,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -628,8 +691,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -806,6 +867,74 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get logs by workspace agent
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/logs \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /workspaceagents/{workspaceagent}/logs`
### Parameters
| Name | In | Type | Required | Description |
| ---------------- | ----- | ------------ | -------- | -------------------------------------------- |
| `workspaceagent` | path | string(uuid) | true | Workspace agent ID |
| `before` | query | integer | false | Before log id |
| `after` | query | integer | false | After log id |
| `follow` | query | boolean | false | Follow log stream |
| `no_compression` | query | boolean | false | Disable compression for WebSocket connection |
### Example responses
> 200 Response
```json
[
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
}
]
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceAgentLog](schemas.md#codersdkworkspaceagentlog) |
<h3 id="get-logs-by-workspace-agent-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| -------------- | ------------------------------------------------ | -------- | ------------ | ----------- |
| `[array item]` | array | false | | |
| `» created_at` | string(date-time) | false | | |
| `» id` | integer | false | | |
| `» level` | [codersdk.LogLevel](schemas.md#codersdkloglevel) | false | | |
| `» output` | string | false | | |
#### Enumerated Values
| Property | Value |
| -------- | ------- |
| `level` | `trace` |
| `level` | `debug` |
| `level` | `info` |
| `level` | `warn` |
| `level` | `error` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Open PTY to workspace agent
### Code samples
@ -832,7 +961,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/pty
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get startup logs by workspace agent
## Removed: Get logs by workspace agent
### Code samples
@ -872,11 +1001,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/sta
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceAgentStartupLog](schemas.md#codersdkworkspaceagentstartuplog) |
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceAgentLog](schemas.md#codersdkworkspaceagentlog) |
<h3 id="get-startup-logs-by-workspace-agent-responseschema">Response Schema</h3>
<h3 id="removed:-get-logs-by-workspace-agent-responseschema">Response Schema</h3>
Status Code **200**

32
docs/api/builds.md generated
View File

@ -107,6 +107,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -114,8 +116,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -269,6 +269,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -276,8 +278,6 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -570,6 +570,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -577,8 +579,6 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -659,6 +659,8 @@ Status Code **200**
| `»»»» preferred` | boolean | false | | |
| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | |
| `»» login_before_ready` | boolean | false | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
@ -666,8 +668,6 @@ Status Code **200**
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_logs_length` | integer | false | | |
| `»» startup_logs_overflowed` | boolean | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
@ -828,6 +828,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -835,8 +837,6 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -995,6 +995,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -1002,8 +1004,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -1120,6 +1120,8 @@ Status Code **200**
| `»»»»» preferred` | boolean | false | | |
| `»»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | |
| `»»» login_before_ready` | boolean | false | | Deprecated: Use StartupScriptBehavior instead. |
| `»»» logs_length` | integer | false | | |
| `»»» logs_overflowed` | boolean | false | | |
| `»»» name` | string | false | | |
| `»»» operating_system` | string | false | | |
| `»»» ready_at` | string(date-time) | false | | |
@ -1127,8 +1129,6 @@ Status Code **200**
| `»»» shutdown_script` | string | false | | |
| `»»» shutdown_script_timeout_seconds` | integer | false | | |
| `»»» started_at` | string(date-time) | false | | |
| `»»» startup_logs_length` | integer | false | | |
| `»»» startup_logs_overflowed` | boolean | false | | |
| `»»» startup_script` | string | false | | |
| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»»» startup_script_timeout_seconds` | integer | false | | »»startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
@ -1343,6 +1343,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -1350,8 +1352,6 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,

132
docs/api/schemas.md generated
View File

@ -157,6 +157,26 @@
| ---------------- | ------ | -------- | ------------ | ----------- |
| `json_web_token` | string | true | | |
## agentsdk.Log
```json
{
"created_at": "string",
"level": "trace",
"output": "string",
"source": "startup_script"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
| `source` | [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | |
## agentsdk.Manifest
```json
@ -277,7 +297,7 @@
| `startup_script_timeout` | integer | false | | |
| `vscode_port_proxy_uri` | string | false | | |
## agentsdk.PatchStartupLogs
## agentsdk.PatchLogs
```json
{
@ -285,7 +305,8 @@
{
"created_at": "string",
"level": "trace",
"output": "string"
"output": "string",
"source": "startup_script"
}
]
}
@ -293,9 +314,9 @@
### Properties
| Name | Type | Required | Restrictions | Description |
| ------ | --------------------------------------------------- | -------- | ------------ | ----------- |
| `logs` | array of [agentsdk.StartupLog](#agentsdkstartuplog) | false | | |
| Name | Type | Required | Restrictions | Description |
| ------ | ------------------------------------- | -------- | ------------ | ----------- |
| `logs` | array of [agentsdk.Log](#agentsdklog) | false | | |
## agentsdk.PostAppHealthsRequest
@ -369,24 +390,6 @@
| `subsystem` | [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
| `version` | string | false | | |
## agentsdk.StartupLog
```json
{
"created_at": "string",
"level": "trace",
"output": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
## agentsdk.Stats
```json
@ -5240,6 +5243,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -5247,8 +5252,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -5381,6 +5384,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -5388,8 +5393,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -5423,6 +5426,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| » `[any property]` | [codersdk.DERPRegion](#codersdkderpregion) | false | | |
| `lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | |
| `login_before_ready` | boolean | false | | Deprecated: Use StartupScriptBehavior instead. |
| `logs_length` | integer | false | | |
| `logs_overflowed` | boolean | false | | |
| `name` | string | false | | |
| `operating_system` | string | false | | |
| `ready_at` | string | false | | |
@ -5430,8 +5435,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `shutdown_script` | string | false | | |
| `shutdown_script_timeout_seconds` | integer | false | | |
| `started_at` | string | false | | |
| `startup_logs_length` | integer | false | | |
| `startup_logs_overflowed` | boolean | false | | |
| `startup_script` | string | false | | |
| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `startup_script_timeout_seconds` | integer | false | | Startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
@ -5583,6 +5586,45 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| ------- | ------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ports` | array of [codersdk.WorkspaceAgentListeningPort](#codersdkworkspaceagentlisteningport) | false | | If there are no ports in the list, nothing should be displayed in the UI. There must not be a "no ports available" message or anything similar, as there will always be no ports displayed on platforms where our port detection logic is unsupported. |
## codersdk.WorkspaceAgentLog
```json
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `id` | integer | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
## codersdk.WorkspaceAgentLogSource
```json
"startup_script"
```
### Properties
#### Enumerated Values
| Value |
| ----------------- |
| `startup_script` |
| `shutdown_script` |
| `kubernetes` |
| `envbox` |
| `envbuilder` |
| `external` |
## codersdk.WorkspaceAgentMetadataDescription
```json
@ -5605,26 +5647,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `script` | string | false | | |
| `timeout` | integer | false | | |
## codersdk.WorkspaceAgentStartupLog
```json
{
"created_at": "2019-08-24T14:15:22Z",
"id": 0,
"level": "trace",
"output": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------- | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `id` | integer | false | | |
| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | |
| `output` | string | false | | |
## codersdk.WorkspaceAgentStartupScriptBehavior
```json
@ -5820,6 +5842,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -5827,8 +5851,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -6129,6 +6151,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -6136,8 +6160,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -6341,6 +6363,8 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -6348,8 +6372,6 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,

16
docs/api/templates.md generated
View File

@ -1620,6 +1620,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -1627,8 +1629,6 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -1709,6 +1709,8 @@ Status Code **200**
| `»»»» preferred` | boolean | false | | |
| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | |
| `»» login_before_ready` | boolean | false | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
@ -1716,8 +1718,6 @@ Status Code **200**
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_logs_length` | integer | false | | |
| `»» startup_logs_overflowed` | boolean | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
@ -2012,6 +2012,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -2019,8 +2021,6 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -2101,6 +2101,8 @@ Status Code **200**
| `»»»» preferred` | boolean | false | | |
| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | |
| `»» login_before_ready` | boolean | false | | Deprecated: Use StartupScriptBehavior instead. |
| `»» logs_length` | integer | false | | |
| `»» logs_overflowed` | boolean | false | | |
| `»» name` | string | false | | |
| `»» operating_system` | string | false | | |
| `»» ready_at` | string(date-time) | false | | |
@ -2108,8 +2110,6 @@ Status Code **200**
| `»» shutdown_script` | string | false | | |
| `»» shutdown_script_timeout_seconds` | integer | false | | |
| `»» started_at` | string(date-time) | false | | |
| `»» startup_logs_length` | integer | false | | |
| `»» startup_logs_overflowed` | boolean | false | | |
| `»» startup_script` | string | false | | |
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |

16
docs/api/workspaces.md generated
View File

@ -135,6 +135,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -142,8 +144,6 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -323,6 +323,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -330,8 +332,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -510,6 +510,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -517,8 +519,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
@ -699,6 +699,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
},
"lifecycle_state": "created",
"login_before_ready": true,
"logs_length": 0,
"logs_overflowed": true,
"name": "string",
"operating_system": "string",
"ready_at": "2019-08-24T14:15:22Z",
@ -706,8 +708,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"shutdown_script": "string",
"shutdown_script_timeout_seconds": 0,
"started_at": "2019-08-24T14:15:22Z",
"startup_logs_length": 0,
"startup_logs_overflowed": true,
"startup_script": "string",
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,

View File

@ -775,11 +775,11 @@ export const getWorkspaceBuildLogs = async (
return response.data
}
export const getWorkspaceAgentStartupLogs = async (
export const getWorkspaceAgentLogs = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentStartupLog[]> => {
const response = await axios.get<TypesGen.WorkspaceAgentStartupLog[]>(
`/api/v2/workspaceagents/${agentID}/startup-logs`,
): Promise<TypesGen.WorkspaceAgentLog[]> => {
const response = await axios.get<TypesGen.WorkspaceAgentLog[]>(
`/api/v2/workspaceagents/${agentID}/logs`,
)
return response.data
}
@ -1284,21 +1284,21 @@ export const watchBuildLogsByTemplateVersionId = (
return socket
}
type WatchStartupLogsOptions = {
type WatchWorkspaceAgentLogsOptions = {
after: number
onMessage: (logs: TypesGen.WorkspaceAgentStartupLog[]) => void
onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void
onDone: () => void
onError: (error: Error) => void
}
export const watchStartupLogs = (
export const watchWorkspaceAgentLogs = (
agentId: string,
{ after, onMessage, onDone, onError }: WatchStartupLogsOptions,
{ after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions,
) => {
// WebSocket compression in Safari (confirmed in 16.5) is broken when
// the server sends large messages. The following error is seen:
//
// WebSocket connection to 'wss://.../startup-logs?follow&after=0' failed: The operation couldnt be completed. Protocol error
// WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldnt be completed. Protocol error
//
const noCompression =
userAgentParser(navigator.userAgent).browser.name === "Safari"
@ -1307,11 +1307,11 @@ export const watchStartupLogs = (
const proto = location.protocol === "https:" ? "wss:" : "ws:"
const socket = new WebSocket(
`${proto}//${location.host}/api/v2/workspaceagents/${agentId}/startup-logs?follow&after=${after}${noCompression}`,
`${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`,
)
socket.binaryType = "blob"
socket.addEventListener("message", (event) => {
const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentStartupLog[]
const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]
onMessage(logs)
})
socket.addEventListener("error", () => {

View File

@ -1302,8 +1302,8 @@ export interface WorkspaceAgent {
readonly startup_script?: string
readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior
readonly startup_script_timeout_seconds: number
readonly startup_logs_length: number
readonly startup_logs_overflowed: boolean
readonly logs_length: number
readonly logs_overflowed: boolean
readonly directory?: string
readonly expanded_directory?: string
readonly version: string
@ -1336,6 +1336,14 @@ export interface WorkspaceAgentListeningPortsResponse {
readonly ports: WorkspaceAgentListeningPort[]
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentLog {
readonly id: number
readonly created_at: string
readonly output: string
readonly level: LogLevel
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentMetadata {
readonly result: WorkspaceAgentMetadataResult
@ -1359,14 +1367,6 @@ export interface WorkspaceAgentMetadataResult {
readonly error: string
}
// From codersdk/workspaceagents.go
export interface WorkspaceAgentStartupLog {
readonly id: number
readonly created_at: string
readonly output: string
readonly level: LogLevel
}
// From codersdk/workspaceapps.go
export interface WorkspaceApp {
readonly id: string
@ -1820,6 +1820,23 @@ export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [
"starting",
]
// From codersdk/workspaceagents.go
export type WorkspaceAgentLogSource =
| "envbox"
| "envbuilder"
| "external"
| "kubernetes"
| "shutdown_script"
| "startup_script"
export const WorkspaceAgentLogSources: WorkspaceAgentLogSource[] = [
"envbox",
"envbuilder",
"external",
"kubernetes",
"shutdown_script",
"startup_script",
]
// From codersdk/workspaceagents.go
export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking"
export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] =

View File

@ -215,7 +215,7 @@ Started.args = {
...Example.args,
agent: {
...MockWorkspaceAgentReady,
startup_logs_length: 1,
logs_length: 1,
},
}

View File

@ -54,7 +54,7 @@ export interface AgentRowProps {
hideVSCodeDesktopButton?: boolean
serverVersion: string
onUpdateAgent: () => void
storybookStartupLogs?: LineWithID[]
storybookLogs?: LineWithID[]
storybookAgentMetadata?: WorkspaceAgentMetadata[]
}
@ -66,7 +66,7 @@ export const AgentRow: FC<AgentRowProps> = ({
hideVSCodeDesktopButton,
serverVersion,
onUpdateAgent,
storybookStartupLogs,
storybookLogs,
storybookAgentMetadata,
sshPrefix,
}) => {
@ -75,10 +75,10 @@ export const AgentRow: FC<AgentRowProps> = ({
context: { agentID: agent.id },
services: process.env.STORYBOOK
? {
getStartupLogs: async () => {
return storybookStartupLogs || []
getLogs: async () => {
return storybookLogs || []
},
streamStartupLogs: () => async () => {
streamLogs: () => async () => {
// noop
},
}
@ -93,39 +93,38 @@ export const AgentRow: FC<AgentRowProps> = ({
((agent.status === "connected" && hasAppsToDisplay) ||
agent.status === "connecting")
const hasStartupFeatures =
Boolean(agent.startup_logs_length) ||
Boolean(logsMachine.context.startupLogs?.length)
Boolean(agent.logs_length) || Boolean(logsMachine.context.logs?.length)
const { proxy } = useProxy()
const [showStartupLogs, setShowStartupLogs] = useState(
const [showLogs, setShowLogs] = useState(
["starting", "start_timeout"].includes(agent.lifecycle_state) &&
hasStartupFeatures,
)
useEffect(() => {
setShowStartupLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures)
setShowLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures)
}, [agent.lifecycle_state, hasStartupFeatures])
// External applications can provide startup logs for an agent during it's spawn.
// These could be Kubernetes logs, or other logs that are useful to the user.
// For this reason, we want to fetch these logs when the agent is starting.
useEffect(() => {
if (agent.lifecycle_state === "starting") {
sendLogsEvent("FETCH_STARTUP_LOGS")
sendLogsEvent("FETCH_LOGS")
}
}, [sendLogsEvent, agent.lifecycle_state])
useEffect(() => {
// We only want to fetch logs when they are actually shown,
// otherwise we can make a lot of requests that aren't necessary.
if (showStartupLogs && logsMachine.can("FETCH_STARTUP_LOGS")) {
sendLogsEvent("FETCH_STARTUP_LOGS")
if (showLogs && logsMachine.can("FETCH_LOGS")) {
sendLogsEvent("FETCH_LOGS")
}
}, [logsMachine, sendLogsEvent, showStartupLogs])
}, [logsMachine, sendLogsEvent, showLogs])
const logListRef = useRef<List>(null)
const logListDivRef = useRef<HTMLDivElement>(null)
const startupLogs = useMemo(() => {
const allLogs = logsMachine.context.startupLogs || []
const allLogs = logsMachine.context.logs || []
const logs = [...allLogs]
if (agent.startup_logs_overflowed) {
if (agent.logs_overflowed) {
logs.push({
id: -1,
level: "error",
@ -134,7 +133,7 @@ export const AgentRow: FC<AgentRowProps> = ({
})
}
return logs
}, [logsMachine.context.startupLogs, agent.startup_logs_overflowed])
}, [logsMachine.context.logs, agent.logs_overflowed])
const [bottomOfLogs, setBottomOfLogs] = useState(true)
// This is a layout effect to remove flicker when we're scrolling to the bottom.
useLayoutEffect(() => {
@ -142,7 +141,7 @@ export const AgentRow: FC<AgentRowProps> = ({
if (bottomOfLogs && logListRef.current) {
logListRef.current.scrollToItem(startupLogs.length - 1, "end")
}
}, [showStartupLogs, startupLogs, logListRef, bottomOfLogs])
}, [showLogs, startupLogs, logListRef, bottomOfLogs])
// This is a bit of a hack on the react-window API to get the scroll position.
// If we're scrolled to the bottom, we want to keep the list scrolled to the bottom.
@ -284,7 +283,7 @@ export const AgentRow: FC<AgentRowProps> = ({
{hasStartupFeatures && (
<div className={styles.logsPanel}>
<Collapse in={showStartupLogs}>
<Collapse in={showLogs}>
<AutoSizer disableHeight>
{({ width }) => (
<List
@ -310,14 +309,14 @@ export const AgentRow: FC<AgentRowProps> = ({
</Collapse>
<div className={styles.logsPanelButtons}>
{showStartupLogs ? (
{showLogs ? (
<button
className={combineClasses([
styles.logsPanelButton,
styles.toggleLogsButton,
])}
onClick={() => {
setShowStartupLogs((v) => !v)
setShowLogs((v) => !v)
}}
>
<CloseDropdown />
@ -330,7 +329,7 @@ export const AgentRow: FC<AgentRowProps> = ({
styles.toggleLogsButton,
])}
onClick={() => {
setShowStartupLogs((v) => !v)
setShowLogs((v) => !v)
}}
>
<OpenDropdown />

View File

@ -43,10 +43,12 @@ const renderWorkspacePage = async () => {
jest
.spyOn(api, "getDeploymentValues")
.mockResolvedValueOnce(MockDeploymentConfig)
jest.spyOn(api, "watchStartupLogs").mockImplementation((_, options) => {
options.onDone()
return new WebSocket("")
})
jest
.spyOn(api, "watchWorkspaceAgentLogs")
.mockImplementation((_, options) => {
options.onDone()
return new WebSocket("")
})
renderWithAuth(<WorkspacePage />, {
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
path: "/:username/:workspace",

View File

@ -555,8 +555,8 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
lifecycle_state: "starting",
login_before_ready: false, // Deprecated.
startup_script_behavior: "blocking",
startup_logs_length: 0,
startup_logs_overflowed: false,
logs_length: 0,
logs_overflowed: false,
startup_script_timeout_seconds: 120,
shutdown_script_timeout_seconds: 120,
subsystem: "envbox",
@ -1789,7 +1789,7 @@ export const MockDeploymentSSH: TypesGen.SSHConfigResponse = {
ssh_config_options: {},
}
export const MockStartupLogs: TypesGen.WorkspaceAgentStartupLog[] = [
export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [
{
id: 166663,
created_at: "2023-05-04T11:30:41.402072Z",

View File

@ -384,7 +384,7 @@ export const handlers = [
return res(ctx.status(200), ctx.json(M.MockDeploymentSSH))
}),
rest.get("/api/v2/workspaceagents/:agent/startup-logs", (_, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockStartupLogs))
rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs))
}),
]

View File

@ -16,21 +16,21 @@ export const workspaceAgentLogsMachine = createMachine(
schema: {
events: {} as
| {
type: "ADD_STARTUP_LOGS"
type: "ADD_LOGS"
logs: LineWithID[]
}
| {
type: "FETCH_STARTUP_LOGS"
type: "FETCH_LOGS"
}
| {
type: "STARTUP_DONE"
type: "DONE"
},
context: {} as {
agentID: string
startupLogs?: LineWithID[]
logs?: LineWithID[]
},
services: {} as {
getStartupLogs: {
getLogs: {
data: LineWithID[]
}
},
@ -40,29 +40,29 @@ export const workspaceAgentLogsMachine = createMachine(
states: {
waiting: {
on: {
FETCH_STARTUP_LOGS: "loading",
FETCH_LOGS: "loading",
},
},
loading: {
invoke: {
src: "getStartupLogs",
src: "getLogs",
onDone: {
target: "watchStartupLogs",
actions: ["assignStartupLogs"],
target: "watchLogs",
actions: ["assignLogs"],
},
},
},
watchStartupLogs: {
id: "watchingStartupLogs",
watchLogs: {
id: "watchingLogs",
invoke: {
id: "streamStartupLogs",
src: "streamStartupLogs",
id: "streamLogs",
src: "streamLogs",
},
on: {
ADD_STARTUP_LOGS: {
actions: "addStartupLogs",
ADD_LOGS: {
actions: "addLogs",
},
STARTUP_DONE: {
DONE: {
target: "loaded",
},
},
@ -74,8 +74,8 @@ export const workspaceAgentLogsMachine = createMachine(
},
{
services: {
getStartupLogs: (ctx) =>
API.getWorkspaceAgentStartupLogs(ctx.agentID).then((data) =>
getLogs: (ctx) =>
API.getWorkspaceAgentLogs(ctx.agentID).then((data) =>
data.map((log) => ({
id: log.id,
level: log.level || "info",
@ -83,17 +83,17 @@ export const workspaceAgentLogsMachine = createMachine(
time: log.created_at,
})),
),
streamStartupLogs: (ctx) => async (callback) => {
streamLogs: (ctx) => async (callback) => {
let after = 0
if (ctx.startupLogs && ctx.startupLogs.length > 0) {
after = ctx.startupLogs[ctx.startupLogs.length - 1].id
if (ctx.logs && ctx.logs.length > 0) {
after = ctx.logs[ctx.logs.length - 1].id
}
const socket = API.watchStartupLogs(ctx.agentID, {
const socket = API.watchWorkspaceAgentLogs(ctx.agentID, {
after,
onMessage: (logs) => {
callback({
type: "ADD_STARTUP_LOGS",
type: "ADD_LOGS",
logs: logs.map((log) => ({
id: log.id,
level: log.level || "info",
@ -103,7 +103,7 @@ export const workspaceAgentLogsMachine = createMachine(
})
},
onDone: () => {
callback({ type: "STARTUP_DONE" })
callback({ type: "DONE" })
},
onError: (error) => {
console.error(error)
@ -116,12 +116,12 @@ export const workspaceAgentLogsMachine = createMachine(
},
},
actions: {
assignStartupLogs: assign({
startupLogs: (_, { data }) => data,
assignLogs: assign({
logs: (_, { data }) => data,
}),
addStartupLogs: assign({
startupLogs: (context, event) => {
const previousLogs = context.startupLogs ?? []
addLogs: assign({
logs: (context, event) => {
const previousLogs = context.logs ?? []
return [...previousLogs, ...event.logs]
},
}),

View File

@ -46,7 +46,7 @@ export default defineConfig({
secure: process.env.NODE_ENV === "production",
configure: (proxy) => {
// Vite does not catch socket errors, and stops the webserver.
// As /startup-logs endpoint can return HTTP 4xx status, we need to embrace
// As /logs endpoint can return HTTP 4xx status, we need to embrace
// Vite with a custom error handler to prevent from quitting.
proxy.on("proxyReqWs", (proxyReq, req, socket) => {
if (process.env.NODE_ENV === "development") {