From 28b57128a62888b0077db4e5cac99345a7481a10 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 21 Jul 2023 15:17:51 +0000 Subject: [PATCH 01/13] 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. --- agent/agent.go | 4 +- agent/agenttest/client.go | 6 +- cli/cliui/agent.go | 8 +- cli/cliui/agent_test.go | 70 ++--- cmd/cliui/main.go | 10 +- coderd/apidoc/docs.go | 227 +++++++-------- coderd/apidoc/swagger.json | 211 +++++++------- coderd/coderd.go | 8 +- coderd/database/dbauthz/dbauthz.go | 49 +--- coderd/database/dbauthz/dbauthz_test.go | 14 +- coderd/database/dbfake/dbfake.go | 158 +++++----- coderd/database/dbmetrics/dbmetrics.go | 50 ++-- coderd/database/dbmock/dbmock.go | 98 +++---- coderd/database/dbpurge/dbpurge.go | 2 +- coderd/database/dump.sql | 45 +-- coderd/database/errors.go | 2 +- .../000140_workspace_agent_logs.down.sql | 1 + .../000140_workspace_agent_logs.up.sql | 8 + coderd/database/models.go | 90 +++++- coderd/database/querier.go | 8 +- coderd/database/querier_test.go | 4 +- coderd/database/queries.sql.go | 269 +++++++++--------- coderd/database/queries/workspaceagents.sql | 23 +- coderd/workspaceagents.go | 79 +++-- coderd/workspaceagents_test.go | 12 +- coderd/workspaces.go | 4 +- coderd/wsconncache/wsconncache_test.go | 2 +- codersdk/agentsdk/agentsdk.go | 21 +- codersdk/agentsdk/logs.go | 22 +- codersdk/agentsdk/logs_test.go | 48 ++-- codersdk/workspaceagents.go | 24 +- docs/api/agents.md | 189 ++++++------ docs/api/builds.md | 32 +-- docs/api/schemas.md | 113 ++++---- docs/api/templates.md | 16 +- docs/api/workspaces.md | 16 +- site/src/api/typesGenerated.ts | 28 +- 37 files changed, 1036 insertions(+), 935 deletions(-) create mode 100644 coderd/database/migrations/000140_workspace_agent_logs.down.sql create mode 100644 coderd/database/migrations/000140_workspace_agent_logs.up.sql diff --git a/agent/agent.go b/agent/agent.go index 4b573f56ee5b8..e825860970d5e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -77,7 +77,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) } @@ -920,7 +920,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 diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index c69ff59eb730b..c981637f059b7 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -49,7 +49,7 @@ type Client struct { mu sync.Mutex // Protects following. lifecycleStates []codersdk.WorkspaceAgentLifecycle startup agentsdk.PostStartupRequest - logs []agentsdk.StartupLog + logs []agentsdk.Log } func (c *Client) Manifest(_ context.Context) (agentsdk.Manifest, error) { @@ -150,13 +150,13 @@ func (c *Client) PostStartup(_ context.Context, startup agentsdk.PostStartupRequ 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(_ context.Context, logs agentsdk.PatchStartupLogs) error { +func (c *Client) PatchLogs(_ context.Context, logs agentsdk.PatchLogs) error { c.mu.Lock() defer c.mu.Unlock() if c.PatchWorkspaceLogs != nil { diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go index fcaa1de54ac67..faad31f411f90 100644 --- a/cli/cliui/agent.go +++ b/cli/cliui/agent.go @@ -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 diff --git a/cli/cliui/agent_test.go b/cli/cliui/agent_test.go index e0e0485778f41..c910bf11c301a 100644 --- a/cli/cliui/agent_test.go +++ b/cli/cliui/agent_test.go @@ -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() diff --git a/cmd/cliui/main.go b/cmd/cliui/main.go index cebf354cf44b9..60e8f6536f7a8 100644 --- a/cmd/cliui/main.go +++ b/cmd/cliui/main.go @@ -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 diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1445a0cd3c9ff..4f3e1128cd88d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4458,6 +4458,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": [ @@ -4641,45 +4680,6 @@ const docTemplate = `{ } } }, - "/workspaceagents/me/startup-logs": { - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Agents" - ], - "summary": "Patch workspace agent startup logs", - "operationId": "patch-workspace-agent-startup-logs", - "parameters": [ - { - "description": "Startup logs", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PatchStartupLogs" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } - } - } - } - }, "/workspaceagents/{workspaceagent}": { "get": { "security": [ @@ -4852,36 +4852,7 @@ const docTemplate = `{ } } }, - "/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": [ { @@ -4894,8 +4865,8 @@ const docTemplate = `{ "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", @@ -4936,13 +4907,42 @@ const docTemplate = `{ "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}/watch-metadata": { "get": { "security": [ @@ -6098,6 +6098,23 @@ const docTemplate = `{ } } }, + "agentsdk.Log": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "level": { + "$ref": "#/definitions/codersdk.LogLevel" + }, + "output": { + "type": "string" + }, + "stage": { + "type": "string" + } + } + }, "agentsdk.Manifest": { "type": "object", "properties": { @@ -6155,13 +6172,13 @@ const docTemplate = `{ } } }, - "agentsdk.PatchStartupLogs": { + "agentsdk.PatchLogs": { "type": "object", "properties": { "logs": { "type": "array", "items": { - "$ref": "#/definitions/agentsdk.StartupLog" + "$ref": "#/definitions/agentsdk.Log" } } } @@ -6222,20 +6239,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": { @@ -9950,6 +9953,12 @@ const docTemplate = `{ "description": "Deprecated: Use StartupScriptBehavior instead.", "type": "boolean" }, + "logs_length": { + "type": "integer" + }, + "logs_overflowed": { + "type": "boolean" + }, "name": { "type": "string" }, @@ -9974,12 +9983,6 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, - "startup_logs_length": { - "type": "integer" - }, - "startup_logs_overflowed": { - "type": "boolean" - }, "startup_script": { "type": "string" }, @@ -10087,41 +10090,41 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceAgentMetadataDescription": { + "codersdk.WorkspaceAgentLog": { "type": "object", "properties": { - "display_name": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time" }, - "interval": { + "id": { "type": "integer" }, - "key": { - "type": "string" + "level": { + "$ref": "#/definitions/codersdk.LogLevel" }, - "script": { + "output": { "type": "string" - }, - "timeout": { - "type": "integer" } } }, - "codersdk.WorkspaceAgentStartupLog": { + "codersdk.WorkspaceAgentMetadataDescription": { "type": "object", "properties": { - "created_at": { - "type": "string", - "format": "date-time" + "display_name": { + "type": "string" }, - "id": { + "interval": { "type": "integer" }, - "level": { - "$ref": "#/definitions/codersdk.LogLevel" + "key": { + "type": "string" }, - "output": { + "script": { "type": "string" + }, + "timeout": { + "type": "integer" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4119f580e91ad..8a79506a5be0c 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3924,6 +3924,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": [ @@ -4083,39 +4116,6 @@ } } }, - "/workspaceagents/me/startup-logs": { - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Agents"], - "summary": "Patch workspace agent startup logs", - "operationId": "patch-workspace-agent-startup-logs", - "parameters": [ - { - "description": "Startup logs", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/agentsdk.PatchStartupLogs" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } - } - } - } - }, "/workspaceagents/{workspaceagent}": { "get": { "security": [ @@ -4270,34 +4270,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": [ { @@ -4306,8 +4279,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", @@ -4348,13 +4321,40 @@ "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}/watch-metadata": { "get": { "security": [ @@ -5372,6 +5372,23 @@ } } }, + "agentsdk.Log": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "level": { + "$ref": "#/definitions/codersdk.LogLevel" + }, + "output": { + "type": "string" + }, + "stage": { + "type": "string" + } + } + }, "agentsdk.Manifest": { "type": "object", "properties": { @@ -5429,13 +5446,13 @@ } } }, - "agentsdk.PatchStartupLogs": { + "agentsdk.PatchLogs": { "type": "object", "properties": { "logs": { "type": "array", "items": { - "$ref": "#/definitions/agentsdk.StartupLog" + "$ref": "#/definitions/agentsdk.Log" } } } @@ -5496,20 +5513,6 @@ } } }, - "agentsdk.StartupLog": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "level": { - "$ref": "#/definitions/codersdk.LogLevel" - }, - "output": { - "type": "string" - } - } - }, "agentsdk.Stats": { "type": "object", "properties": { @@ -9004,6 +9007,12 @@ "description": "Deprecated: Use StartupScriptBehavior instead.", "type": "boolean" }, + "logs_length": { + "type": "integer" + }, + "logs_overflowed": { + "type": "boolean" + }, "name": { "type": "string" }, @@ -9028,12 +9037,6 @@ "type": "string", "format": "date-time" }, - "startup_logs_length": { - "type": "integer" - }, - "startup_logs_overflowed": { - "type": "boolean" - }, "startup_script": { "type": "string" }, @@ -9141,41 +9144,41 @@ } } }, - "codersdk.WorkspaceAgentMetadataDescription": { + "codersdk.WorkspaceAgentLog": { "type": "object", "properties": { - "display_name": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time" }, - "interval": { + "id": { "type": "integer" }, - "key": { - "type": "string" + "level": { + "$ref": "#/definitions/codersdk.LogLevel" }, - "script": { + "output": { "type": "string" - }, - "timeout": { - "type": "integer" } } }, - "codersdk.WorkspaceAgentStartupLog": { + "codersdk.WorkspaceAgentMetadataDescription": { "type": "object", "properties": { - "created_at": { - "type": "string", - "format": "date-time" + "display_name": { + "type": "string" }, - "id": { + "interval": { "type": "integer" }, - "level": { - "$ref": "#/definitions/codersdk.LogLevel" + "key": { + "type": "string" }, - "output": { + "script": { "type": "string" + }, + "timeout": { + "type": "integer" } } }, diff --git a/coderd/coderd.go b/coderd/coderd.go index 5ac7881ccdeb4..209cfc5d29d63 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -757,7 +757,9 @@ 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) + // /startup prefix is supported for backwards compatibility - remove in October, 2023. + r.Patch("/startup-logs", api.patchWorkspaceAgentLogs) + r.Patch("/logs", api.patchWorkspaceAgentLogs) r.Post("/app-health", api.postWorkspaceAppHealth) r.Get("/gitauth", api.workspaceAgentsGitAuth) r.Get("/gitsshkey", api.agentGitSSHKey) @@ -781,7 +783,9 @@ func New(options *Options) *API { ) r.Get("/", api.workspaceAgent) r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata) - r.Get("/startup-logs", api.workspaceAgentStartupLogs) + // /startup prefix is supported for backwards compatibility - remove in October, 2023. + r.Get("/startup-logs", api.workspaceAgentLogs) + r.Get("/logs", api.workspaceAgentLogs) r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) r.Get("/coordinate", api.workspaceAgentClientCoordinate) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 57bcd12c96354..b0cafe4c0f29e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -722,11 +722,8 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) { return id, nil } -func (q *querier) DeleteOldWorkspaceAgentStartupLogs(ctx context.Context) error { - if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceSystem); err != nil { - return err - } - return q.db.DeleteOldWorkspaceAgentStartupLogs(ctx) +func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error { + panic("not implemented") } func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { @@ -1414,6 +1411,10 @@ 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) { + panic("not implemented") +} + func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { workspace, err := q.db.GetWorkspaceByAgentID(ctx, workspaceAgentID) if err != nil { @@ -1428,14 +1429,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) } @@ -1896,6 +1889,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) { + panic("not implemented") +} + 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. @@ -1906,10 +1903,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. @@ -2374,6 +2367,10 @@ 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 { + panic("not implemented") +} + func (q *querier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg database.UpdateWorkspaceAgentMetadataParams) error { workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.WorkspaceAgentID) if err != nil { @@ -2406,24 +2403,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) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6a4f09260b7ab..e427ca9840346 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -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{}) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index b1ae1e05b4cb4..6424627c3058c 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -60,7 +60,7 @@ func New() database.Store { templateVersions: make([]database.TemplateVersion, 0), templates: make([]database.TemplateTable, 0), workspaceAgentStats: make([]database.WorkspaceAgentStat, 0), - workspaceAgentLogs: make([]database.WorkspaceAgentStartupLog, 0), + workspaceAgentLogs: make([]database.WorkspaceAgentLog, 0), workspaceBuilds: make([]database.WorkspaceBuild, 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.WorkspaceBuild workspaceBuildParameters []database.WorkspaceBuildParameter @@ -560,6 +560,50 @@ func isNotNull(v interface{}) bool { // these methods remain unimplemented in the FakeQuerier. var ErrUnimplemented = xerrors.New("unimplemented") +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], + 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_startup_logs_length", + Table: "workspace_agents", + } + } + agent.LogsLength += outputLength + q.workspaceAgents[index] = agent + break + } + q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...) + return logs, nil +} + func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -753,7 +797,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 } @@ -2396,20 +2440,7 @@ func (q *FakeQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, i }, nil } -func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - metadata := make([]database.WorkspaceAgentMetadatum, 0) - for _, m := range q.workspaceAgentMetadata { - if m.WorkspaceAgentID == workspaceAgentID { - metadata = append(metadata, m) - } - } - return metadata, nil -} - -func (q *FakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) { +func (q *FakeQuerier) GetWorkspaceAgentLogsAfter(_ context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) { if err := validateDatabaseType(arg); err != nil { return nil, err } @@ -2417,7 +2448,7 @@ func (q *FakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg d q.mutex.RLock() defer q.mutex.RUnlock() - logs := []database.WorkspaceAgentStartupLog{} + logs := []database.WorkspaceAgentLog{} for _, log := range q.workspaceAgentLogs { if log.AgentID != arg.AgentID { continue @@ -2430,6 +2461,19 @@ func (q *FakeQuerier) GetWorkspaceAgentStartupLogsAfter(_ context.Context, arg d return logs, nil } +func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + metadata := make([]database.WorkspaceAgentMetadatum, 0) + for _, m := range q.workspaceAgentMetadata { + if m.WorkspaceAgentID == workspaceAgentID { + metadata = append(metadata, m) + } + } + return metadata, nil +} + func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter time.Time) ([]database.GetWorkspaceAgentStatsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3737,50 +3781,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 @@ -4670,6 +4670,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() @@ -4715,23 +4732,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 diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index e202f7bbc2992..2332b273dbd8e 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -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 { @@ -753,6 +753,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) @@ -760,13 +767,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) @@ -1166,6 +1166,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) @@ -1173,13 +1180,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) @@ -1453,6 +1453,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) @@ -1467,13 +1474,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) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 2a63a4c83f2c4..5c72eb9a03491 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -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. @@ -1556,34 +1556,34 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentLifecycleStateByID(arg0, arg1 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLifecycleStateByID), arg0, arg1) } -// GetWorkspaceAgentMetadata mocks base method. -func (m *MockStore) GetWorkspaceAgentMetadata(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { +// 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, "GetWorkspaceAgentMetadata", arg0, arg1) - ret0, _ := ret[0].([]database.WorkspaceAgentMetadatum) + ret := m.ctrl.Call(m, "GetWorkspaceAgentLogsAfter", arg0, arg1) + ret0, _ := ret[0].([]database.WorkspaceAgentLog) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentMetadata indicates an expected call of GetWorkspaceAgentMetadata. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 interface{}) *gomock.Call { +// 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, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLogsAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLogsAfter), arg0, arg1) } -// GetWorkspaceAgentStartupLogsAfter mocks base method. -func (m *MockStore) GetWorkspaceAgentStartupLogsAfter(arg0 context.Context, arg1 database.GetWorkspaceAgentStartupLogsAfterParams) ([]database.WorkspaceAgentStartupLog, error) { +// GetWorkspaceAgentMetadata mocks base method. +func (m *MockStore) GetWorkspaceAgentMetadata(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentMetadatum, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceAgentStartupLogsAfter", arg0, arg1) - ret0, _ := ret[0].([]database.WorkspaceAgentStartupLog) + ret := m.ctrl.Call(m, "GetWorkspaceAgentMetadata", arg0, arg1) + ret0, _ := ret[0].([]database.WorkspaceAgentMetadatum) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetWorkspaceAgentStartupLogsAfter indicates an expected call of GetWorkspaceAgentStartupLogsAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentStartupLogsAfter(arg0, arg1 interface{}) *gomock.Call { +// GetWorkspaceAgentMetadata indicates an expected call of GetWorkspaceAgentMetadata. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(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) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1) } // GetWorkspaceAgentStats mocks base method. @@ -2450,6 +2450,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() @@ -2464,21 +2479,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() @@ -3063,6 +3063,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() @@ -3091,20 +3105,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() diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go index abafcb4559f25..de7c2db67ef4b 100644 --- a/coderd/database/dbpurge/dbpurge.go +++ b/coderd/database/dbpurge/dbpurge.go @@ -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) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f486d162e15c4..9983ad472b866 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -128,6 +128,14 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM ( 'off' ); +CREATE TYPE workspace_agent_log_source AS ENUM ( + 'startup_script', + 'kubernetes_logs', + 'envbox', + 'envbuilder', + 'external' +); + CREATE TYPE workspace_agent_subsystem AS ENUM ( 'envbuilder', 'envbox', @@ -652,6 +660,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, @@ -664,14 +681,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 @@ -679,7 +688,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, @@ -729,13 +738,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.'; @@ -756,9 +765,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.'; @@ -883,7 +892,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_resource_metadata ALTER COLUMN id SET DEFAULT nextval('workspace_resource_metadata_id_seq'::regclass); @@ -989,7 +998,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 @@ -1072,7 +1081,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); @@ -1157,7 +1166,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 diff --git a/coderd/database/errors.go b/coderd/database/errors.go index bacddf8223408..61f87a6f633ed 100644 --- a/coderd/database/errors.go +++ b/coderd/database/errors.go @@ -52,7 +52,7 @@ func IsQueryCanceledError(err error) bool { func IsStartupLogsLimitError(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 diff --git a/coderd/database/migrations/000140_workspace_agent_logs.down.sql b/coderd/database/migrations/000140_workspace_agent_logs.down.sql new file mode 100644 index 0000000000000..1e64bc94cb403 --- /dev/null +++ b/coderd/database/migrations/000140_workspace_agent_logs.down.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_agent_logs RENAME TO workspace_agent_startup_logs; diff --git a/coderd/database/migrations/000140_workspace_agent_logs.up.sql b/coderd/database/migrations/000140_workspace_agent_logs.up.sql new file mode 100644 index 0000000000000..e5a412a95a666 --- /dev/null +++ b/coderd/database/migrations/000140_workspace_agent_logs.up.sql @@ -0,0 +1,8 @@ +BEGIN; +CREATE TYPE workspace_agent_log_source AS ENUM ('startup_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; diff --git a/coderd/database/models.go b/coderd/database/models.go index 2fb9fbb244abd..33e91613be26c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1169,6 +1169,73 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState { } } +type WorkspaceAgentLogSource string + +const ( + WorkspaceAgentLogSourceStartupScript WorkspaceAgentLogSource = "startup_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, + WorkspaceAgentLogSourceKubernetesLogs, + WorkspaceAgentLogSourceEnvbox, + WorkspaceAgentLogSourceEnvbuilder, + WorkspaceAgentLogSourceExternal: + return true + } + return false +} + +func AllWorkspaceAgentLogSourceValues() []WorkspaceAgentLogSource { + return []WorkspaceAgentLogSource{ + WorkspaceAgentLogSourceStartupScript, + WorkspaceAgentLogSourceKubernetesLogs, + WorkspaceAgentLogSourceEnvbox, + WorkspaceAgentLogSourceEnvbuilder, + WorkspaceAgentLogSourceExternal, + } +} + type WorkspaceAgentSubsystem string const ( @@ -1787,10 +1854,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 @@ -1799,6 +1866,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"` @@ -1811,14 +1887,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"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 05b5a07acdf32..3cc504bab5a26 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -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) @@ -131,8 +131,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) @@ -201,8 +201,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) (WorkspaceBuild, error) @@ -246,9 +246,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) (WorkspaceBuild, error) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 2d41cb7458c72..4c3219b151279 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -111,7 +111,7 @@ 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"}, @@ -122,7 +122,7 @@ func TestInsertWorkspaceAgentStartupLogs(t *testing.T) { 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"}, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e5786aad31a63..856d7c73ff1ef 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5636,22 +5636,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 @@ -5691,8 +5691,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, @@ -5703,7 +5703,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 @@ -5741,8 +5741,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, @@ -5753,7 +5753,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 @@ -5793,8 +5793,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, @@ -5827,34 +5827,39 @@ func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id return i, err } -const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many +const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many SELECT - workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at + agent_id, created_at, output, id, level, source FROM - workspace_agent_metadata + workspace_agent_logs WHERE - workspace_agent_id = $1 + agent_id = $1 + AND ( + id > $2 + ) ORDER BY id ASC ` -func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, workspaceAgentID) +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 []WorkspaceAgentMetadatum + var items []WorkspaceAgentLog for rows.Next() { - var i WorkspaceAgentMetadatum + var i WorkspaceAgentLog if err := rows.Scan( - &i.WorkspaceAgentID, - &i.DisplayName, - &i.Key, - &i.Script, - &i.Value, - &i.Error, - &i.Timeout, - &i.Interval, - &i.CollectedAt, + &i.AgentID, + &i.CreatedAt, + &i.Output, + &i.ID, + &i.Level, + &i.Source, ); err != nil { return nil, err } @@ -5869,38 +5874,34 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAge return items, nil } -const getWorkspaceAgentStartupLogsAfter = `-- name: GetWorkspaceAgentStartupLogsAfter :many +const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many SELECT - agent_id, created_at, output, id, level + workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at FROM - workspace_agent_startup_logs + workspace_agent_metadata WHERE - agent_id = $1 - AND ( - id > $2 - ) ORDER BY id ASC + workspace_agent_id = $1 ` -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) +func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentMetadatum, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, workspaceAgentID) if err != nil { return nil, err } defer rows.Close() - var items []WorkspaceAgentStartupLog + var items []WorkspaceAgentMetadatum for rows.Next() { - var i WorkspaceAgentStartupLog + var i WorkspaceAgentMetadatum if err := rows.Scan( - &i.AgentID, - &i.CreatedAt, - &i.Output, - &i.ID, - &i.Level, + &i.WorkspaceAgentID, + &i.DisplayName, + &i.Key, + &i.Script, + &i.Value, + &i.Error, + &i.Timeout, + &i.Interval, + &i.CollectedAt, ); err != nil { return nil, err } @@ -5917,7 +5918,7 @@ func (q *sqlQuerier) GetWorkspaceAgentStartupLogsAfter(ctx context.Context, arg 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 @@ -5961,8 +5962,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, @@ -5982,7 +5983,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) { @@ -6022,8 +6023,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, @@ -6044,7 +6045,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 @@ -6100,8 +6101,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, @@ -6146,7 +6147,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 { @@ -6226,8 +6227,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, @@ -6236,85 +6237,54 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa return i, err } -const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec -INSERT INTO - workspace_agent_metadata ( - workspace_agent_id, - display_name, - key, - script, - timeout, - interval - ) -VALUES - ($1, $2, $3, $4, $5, $6) -` - -type InsertWorkspaceAgentMetadataParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - DisplayName string `db:"display_name" json:"display_name"` - Key string `db:"key" json:"key"` - Script string `db:"script" json:"script"` - Timeout int64 `db:"timeout" json:"timeout"` - Interval int64 `db:"interval" json:"interval"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error { - _, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata, - arg.WorkspaceAgentID, - arg.DisplayName, - arg.Key, - arg.Script, - arg.Timeout, - arg.Interval, - ) - return err -} - -const insertWorkspaceAgentStartupLogs = `-- name: InsertWorkspaceAgentStartupLogs :many +const insertWorkspaceAgentLogs = `-- name: InsertWorkspaceAgentLogs :many WITH new_length AS ( UPDATE workspace_agents SET - startup_logs_length = startup_logs_length + $5 WHERE workspace_agents.id = $1 + logs_length = logs_length + $6 WHERE workspace_agents.id = $1 ) INSERT INTO - workspace_agent_startup_logs (agent_id, created_at, output, level) + workspace_agent_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 + 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 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"` +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) InsertWorkspaceAgentStartupLogs(ctx context.Context, arg InsertWorkspaceAgentStartupLogsParams) ([]WorkspaceAgentStartupLog, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentStartupLogs, +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 []WorkspaceAgentStartupLog + var items []WorkspaceAgentLog for rows.Next() { - var i WorkspaceAgentStartupLog + var i WorkspaceAgentLog if err := rows.Scan( &i.AgentID, &i.CreatedAt, &i.Output, &i.ID, &i.Level, + &i.Source, ); err != nil { return nil, err } @@ -6329,6 +6299,41 @@ func (q *sqlQuerier) InsertWorkspaceAgentStartupLogs(ctx context.Context, arg In return items, nil } +const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec +INSERT INTO + workspace_agent_metadata ( + workspace_agent_id, + display_name, + key, + script, + timeout, + interval + ) +VALUES + ($1, $2, $3, $4, $5, $6) +` + +type InsertWorkspaceAgentMetadataParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + DisplayName string `db:"display_name" json:"display_name"` + Key string `db:"key" json:"key"` + Script string `db:"script" json:"script"` + Timeout int64 `db:"timeout" json:"timeout"` + Interval int64 `db:"interval" json:"interval"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error { + _, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata, + arg.WorkspaceAgentID, + arg.DisplayName, + arg.Key, + arg.Script, + arg.Timeout, + arg.Interval, + ) + return err +} + const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec UPDATE workspace_agents @@ -6391,6 +6396,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 @@ -6450,25 +6474,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' ` diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 03168685aa37b..2cdcb7239ae75 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -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) 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'); diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index f8d5e10f62de3..43eba4c821d95 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -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 } @@ -280,7 +280,7 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R level = append(level, parsedLevel) } - logs, err := api.Database.InsertWorkspaceAgentStartupLogs(ctx, database.InsertWorkspaceAgentStartupLogsParams{ + logs, err := api.Database.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{ AgentID: workspaceAgent.ID, CreatedAt: createdAt, Output: output, @@ -295,22 +295,22 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R }) 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 +334,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 +343,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.publishWorkspaceAgentStartupLogsUpdate(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 +374,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 +386,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 +415,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 +430,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 +471,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 +487,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 +496,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 +504,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 +522,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 +530,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 +568,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 } @@ -1165,8 +1164,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, @@ -1997,16 +1996,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, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 6afec803bbc49..3761d9b59743c 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -210,8 +210,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", @@ -229,7 +229,7 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) { defer func() { _ = closer.Close() }() - var logChunk []codersdk.WorkspaceAgentStartupLog + var logChunk []codersdk.WorkspaceAgentLog select { case <-ctx.Done(): case logChunk = <-logs: @@ -277,8 +277,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), }}, @@ -296,7 +296,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 } } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8ac07b5ddd51e..bd8bafc6fed98 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1263,12 +1263,12 @@ 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) publishWorkspaceAgentStartupLogsUpdate(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)) } - 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)) } diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go index 276e528313751..24ad5751d202e 100644 --- a/coderd/wsconncache/wsconncache_test.go +++ b/coderd/wsconncache/wsconncache_test.go @@ -256,7 +256,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 } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 90f15eff649e3..6bac6aa5509c4 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -567,19 +567,20 @@ func (c *Client) PostStartup(ctx context.Context, req PostStartupRequest) error return nil } -type StartupLog struct { +type Log struct { CreatedAt time.Time `json:"created_at"` Output string `json:"output"` Level codersdk.LogLevel `json:"level"` + Stage string `json:"stage"` } -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 { +func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error { res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/startup-logs", req) if err != nil { return err @@ -683,13 +684,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"` } diff --git a/codersdk/agentsdk/logs.go b/codersdk/agentsdk/logs.go index 0185085b0d4b8..bcb323fddacb5 100644 --- a/codersdk/agentsdk/logs.go +++ b/codersdk/agentsdk/logs.go @@ -18,7 +18,7 @@ import ( type startupLogsWriter struct { buf bytes.Buffer // Buffer to track partial lines. ctx context.Context - send func(ctx context.Context, log ...StartupLog) error + send func(ctx context.Context, log ...Log) error level codersdk.LogLevel } @@ -39,7 +39,7 @@ 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]), @@ -61,7 +61,7 @@ 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(), @@ -78,7 +78,7 @@ 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, level codersdk.LogLevel) io.WriteCloser { return &startupLogsWriter{ ctx: ctx, send: sender, @@ -86,12 +86,12 @@ func StartupLogsWriter(ctx context.Context, sender func(ctx context.Context, log } } -// 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 +100,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 +110,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 +150,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 +185,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()) diff --git a/codersdk/agentsdk/logs_test.go b/codersdk/agentsdk/logs_test.go index f14e20a8dfa1e..5644fe325bf6b 100644 --- a/codersdk/agentsdk/logs_test.go +++ b/codersdk/agentsdk/logs_test.go @@ -29,7 +29,7 @@ func TestStartupLogsWriter_Write(t *testing.T) { ctx context.Context level codersdk.LogLevel writes []string - want []agentsdk.StartupLog + want []agentsdk.Log wantErr bool closeFirst bool }{ @@ -38,7 +38,7 @@ func TestStartupLogsWriter_Write(t *testing.T) { ctx: context.Background(), level: codersdk.LogLevelInfo, writes: []string{"hello world\n"}, - want: []agentsdk.StartupLog{ + want: []agentsdk.Log{ { Level: codersdk.LogLevelInfo, Output: "hello world", @@ -50,7 +50,7 @@ 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", @@ -66,7 +66,7 @@ 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: "", @@ -98,7 +98,7 @@ 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", @@ -111,7 +111,7 @@ 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", @@ -127,7 +127,7 @@ 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", @@ -143,7 +143,7 @@ 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", @@ -172,8 +172,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() @@ -233,7 +233,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 +247,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 +260,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 +272,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 +306,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 +336,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", diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index b76ebba9344f5..2b5dba40fd545 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -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"` @@ -557,7 +557,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) WorkspaceAgentStartupLogsAfter(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)) @@ -585,13 +585,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 @@ -619,7 +619,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) @@ -628,7 +628,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 @@ -673,7 +673,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"` @@ -685,3 +685,11 @@ type AgentSubsystem string const ( AgentSubsystemEnvbox AgentSubsystem = "envbox" ) + +type AgentLogSource string + +const ( + AgentLogSourceStartup AgentLogSource = "startup" + AgentLogSourceKubernetes AgentLogSource = "kubernetes" + AgentLogSourceDevcontainer AgentLogSource = "devcontainer" +) diff --git a/docs/api/agents.md b/docs/api/agents.md index 3726196d26857..d82e6b16cd703 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -273,6 +273,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", + "stage": "string" + } + ] +} +``` + +### 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 @@ -468,65 +528,6 @@ 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 - -### Code samples - -```shell -# Example request using curl -curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`PATCH /workspaceagents/me/startup-logs` - -> Body parameter - -```json -{ - "logs": [ - { - "created_at": "string", - "level": "trace", - "output": "string" - } - ] -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | ---------------------------------------------------------------- | -------- | ------------ | -| `body` | body | [agentsdk.PatchStartupLogs](schemas.md#agentsdkpatchstartuplogs) | true | Startup 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 workspace agent by ID ### Code samples @@ -601,6 +602,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", @@ -608,8 +611,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, @@ -786,44 +787,18 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/lis To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Open PTY to workspace agent - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/pty \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /workspaceagents/{workspaceagent}/pty` - -### Parameters - -| Name | In | Type | Required | Description | -| ---------------- | ---- | ------------ | -------- | ------------------ | -| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------------------------ | ------------------- | ------ | -| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Get startup logs by workspace agent +## Get logs by workspace agent ### Code samples ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/startup-logs \ +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}/startup-logs` +`GET /workspaceagents/{workspaceagent}/logs` ### Parameters @@ -852,11 +827,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) | -

Response Schema

+

Response Schema

Status Code **200** @@ -879,3 +854,29 @@ Status Code **200** | `level` | `error` | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Open PTY to workspace agent + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/pty \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/{workspaceagent}/pty` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------------ | -------- | ------------------ | +| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------------------------ | ------------------- | ------ | +| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/builds.md b/docs/api/builds.md index 665b8778b6865..daf7958de387f 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -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, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 075f44fd56a27..d2cc94a8c4618 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -157,6 +157,26 @@ | ---------------- | ------ | -------- | ------------ | ----------- | | `json_web_token` | string | true | | | +## agentsdk.Log + +```json +{ + "created_at": "string", + "level": "trace", + "output": "string", + "stage": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------ | -------------------------------------- | -------- | ------------ | ----------- | +| `created_at` | string | false | | | +| `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | | +| `output` | string | false | | | +| `stage` | string | 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", + "stage": "string" } ] } @@ -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 @@ -4900,6 +4903,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", @@ -4907,8 +4912,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, @@ -5041,6 +5044,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", @@ -5048,8 +5053,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, @@ -5083,6 +5086,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 | | | @@ -5090,8 +5095,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. | @@ -5243,6 +5246,26 @@ 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.WorkspaceAgentMetadataDescription ```json @@ -5265,26 +5288,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 @@ -5480,6 +5483,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", @@ -5487,8 +5492,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, @@ -5787,6 +5790,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", @@ -5794,8 +5799,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, @@ -5999,6 +6002,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", @@ -6006,8 +6011,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, diff --git a/docs/api/templates.md b/docs/api/templates.md index 06b11216727ae..a6c6d897439dc 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1717,6 +1717,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", @@ -1724,8 +1726,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, @@ -1806,6 +1806,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 | | | @@ -1813,8 +1815,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. | @@ -2109,6 +2109,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", @@ -2116,8 +2118,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, @@ -2198,6 +2198,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 | | | @@ -2205,8 +2207,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. | diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 42f317616e55a..390a82448886c 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -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, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a6734f669e96a..294e3f24a1565 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1203,8 +1203,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 @@ -1237,6 +1237,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 @@ -1260,14 +1268,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 @@ -1417,6 +1417,14 @@ export interface WorkspacesResponse { export type APIKeyScope = "all" | "application_connect" export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] +// From codersdk/workspaceagents.go +export type AgentLogSource = "devcontainer" | "kubernetes" | "startup" +export const AgentLogSources: AgentLogSource[] = [ + "devcontainer", + "kubernetes", + "startup", +] + // From codersdk/workspaceagents.go export type AgentSubsystem = "envbox" export const AgentSubsystems: AgentSubsystem[] = ["envbox"] From 9254d65b3ebe705ec2381f508e7b24124f973c00 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 21 Jul 2023 16:01:17 +0000 Subject: [PATCH 02/13] Fix migration order --- coderd/database/dbauthz/dbauthz.go | 29 +++++- coderd/database/dbfake/dbfake.go | 88 +++++++++---------- ...l => 000141_workspace_agent_logs.down.sql} | 0 ...sql => 000141_workspace_agent_logs.up.sql} | 0 codersdk/agentsdk/logs_test.go | 12 +-- 5 files changed, 75 insertions(+), 54 deletions(-) rename coderd/database/migrations/{000140_workspace_agent_logs.down.sql => 000141_workspace_agent_logs.down.sql} (100%) rename coderd/database/migrations/{000140_workspace_agent_logs.up.sql => 000141_workspace_agent_logs.up.sql} (100%) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c027cd60f5b98..4fcfd2a9a4266 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -723,7 +723,10 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) { } func (q *querier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error { - panic("not implemented") + if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceSystem); err != nil { + return err + } + return q.db.DeleteOldWorkspaceAgentLogs(ctx) } func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { @@ -1412,7 +1415,11 @@ func (q *querier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uu } func (q *querier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg database.GetWorkspaceAgentLogsAfterParams) ([]database.WorkspaceAgentLog, error) { - panic("not implemented") + _, 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) { @@ -1890,7 +1897,7 @@ func (q *querier) InsertWorkspaceAgent(ctx context.Context, arg database.InsertW } func (q *querier) InsertWorkspaceAgentLogs(ctx context.Context, arg database.InsertWorkspaceAgentLogsParams) ([]database.WorkspaceAgentLog, error) { - panic("not implemented") + return q.db.InsertWorkspaceAgentLogs(ctx, arg) } func (q *querier) InsertWorkspaceAgentMetadata(ctx context.Context, arg database.InsertWorkspaceAgentMetadataParams) error { @@ -2368,7 +2375,21 @@ func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, ar } func (q *querier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg database.UpdateWorkspaceAgentLogOverflowByIDParams) error { - panic("not implemented") + 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 { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index b8640f1c07507..137649b06c110 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -562,50 +562,6 @@ func isNotNull(v interface{}) bool { // these methods remain unimplemented in the FakeQuerier. var ErrUnimplemented = xerrors.New("unimplemented") -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], - 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_startup_logs_length", - Table: "workspace_agents", - } - } - agent.LogsLength += outputLength - q.workspaceAgents[index] = agent - break - } - q.workspaceAgentLogs = append(q.workspaceAgentLogs, logs...) - return logs, nil -} - func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { return xerrors.New("AcquireLock must only be called within a transaction") } @@ -3765,6 +3721,50 @@ 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], + 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_startup_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() diff --git a/coderd/database/migrations/000140_workspace_agent_logs.down.sql b/coderd/database/migrations/000141_workspace_agent_logs.down.sql similarity index 100% rename from coderd/database/migrations/000140_workspace_agent_logs.down.sql rename to coderd/database/migrations/000141_workspace_agent_logs.down.sql diff --git a/coderd/database/migrations/000140_workspace_agent_logs.up.sql b/coderd/database/migrations/000141_workspace_agent_logs.up.sql similarity index 100% rename from coderd/database/migrations/000140_workspace_agent_logs.up.sql rename to coderd/database/migrations/000141_workspace_agent_logs.up.sql diff --git a/codersdk/agentsdk/logs_test.go b/codersdk/agentsdk/logs_test.go index 5644fe325bf6b..5a76a31af013f 100644 --- a/codersdk/agentsdk/logs_test.go +++ b/codersdk/agentsdk/logs_test.go @@ -261,7 +261,7 @@ func TestStartupLogsSender(t *testing.T) { defer cancel() got := []agentsdk.Log{} - PatchLogs := func(_ context.Context, req agentsdk.PatchLogs) error { + patchLogs := func(_ context.Context, req agentsdk.PatchLogs) error { if tt.patchResp != nil { err := tt.patchResp(req) if err != nil { @@ -272,7 +272,7 @@ func TestStartupLogsSender(t *testing.T) { return nil } - sendLog, flushAndClose := agentsdk.LogsSender(PatchLogs, 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) @@ -306,12 +306,12 @@ func TestStartupLogsSender(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() - PatchLogs := func(_ context.Context, _ agentsdk.PatchLogs) error { + patchLogs := func(_ context.Context, _ agentsdk.PatchLogs) error { assert.Fail(t, "should not be called") return nil } - sendLog, flushAndClose := agentsdk.LogsSender(PatchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug)) + sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug)) defer func() { _ = flushAndClose(ctx) }() @@ -337,12 +337,12 @@ func TestStartupLogsSender(t *testing.T) { defer cancel() var want, got []agentsdk.Log - PatchLogs := func(_ context.Context, req agentsdk.PatchLogs) error { + patchLogs := func(_ context.Context, req agentsdk.PatchLogs) error { got = append(got, req.Logs...) return nil } - sendLog, flushAndClose := agentsdk.LogsSender(PatchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug)) + sendLog, flushAndClose := agentsdk.LogsSender(patchLogs, slogtest.Make(t, nil).Leveled(slog.LevelDebug)) defer func() { _ = flushAndClose(ctx) }() From 8e0f9ba624bd0a369d67f05505942fbd067e657c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 21 Jul 2023 17:16:14 +0000 Subject: [PATCH 03/13] Fix naming --- cli/ssh.go | 2 +- coderd/coderd.go | 6 ++-- coderd/database/querier_test.go | 2 +- coderd/deprecated.go | 29 +++++++++++++++++++ coderd/workspaceagents.go | 2 +- coderd/workspaceagents_test.go | 2 +- coderd/workspaces.go | 6 ++-- codersdk/agentsdk/agentsdk.go | 2 +- codersdk/workspaceagents.go | 4 +-- docs/admin/automation.md | 4 +-- site/src/api/api.ts | 22 +++++++------- site/src/testHelpers/entities.ts | 6 ++-- site/src/testHelpers/handlers.ts | 4 +-- .../workspaceAgentLogsXService.ts | 4 +-- site/vite.config.ts | 2 +- 15 files changed, 62 insertions(+), 35 deletions(-) diff --git a/cli/ssh.go b/cli/ssh.go index 4bf6f021b4881..2db1f4b4e2cb4 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -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 { diff --git a/coderd/coderd.go b/coderd/coderd.go index 209cfc5d29d63..417ccc4fb2a7d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -757,8 +757,7 @@ func New(options *Options) *API { // New agents will use /me/manifest instead. r.Get("/metadata", api.workspaceAgentManifest) r.Post("/startup", api.postWorkspaceAgentStartup) - // /startup prefix is supported for backwards compatibility - remove in October, 2023. - r.Patch("/startup-logs", api.patchWorkspaceAgentLogs) + r.Patch("/startup-logs", api.patchWorkspaceAgentLogsDeprecated) r.Patch("/logs", api.patchWorkspaceAgentLogs) r.Post("/app-health", api.postWorkspaceAppHealth) r.Get("/gitauth", api.workspaceAgentsGitAuth) @@ -783,8 +782,7 @@ func New(options *Options) *API { ) r.Get("/", api.workspaceAgent) r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata) - // /startup prefix is supported for backwards compatibility - remove in October, 2023. - r.Get("/startup-logs", api.workspaceAgentLogs) + r.Get("/startup-logs", api.workspaceAgentLogsDeprecated) r.Get("/logs", api.workspaceAgentLogs) r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 4c3219b151279..65a457dbe1c60 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -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() diff --git a/coderd/deprecated.go b/coderd/deprecated.go index 0911724de884e..59e23b8d48136 100644 --- a/coderd/deprecated.go +++ b/coderd/deprecated.go @@ -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) +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 43eba4c821d95..22deb0b93a516 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -343,7 +343,7 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request) // Publish by the lowest log ID inserted so the // log stream will fetch everything from that point. - api.publishWorkspaceAgentStartupLogsUpdate(ctx, workspaceAgent.ID, agentsdk.LogsNotifyMessage{ + api.publishWorkspaceAgentLogsUpdate(ctx, workspaceAgent.ID, agentsdk.LogsNotifyMessage{ CreatedAfter: lowestLogID - 1, }) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 3761d9b59743c..cf99b7929b663 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -224,7 +224,7 @@ 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() diff --git a/coderd/workspaces.go b/coderd/workspaces.go index c3b35b2f65f59..24a1041da2a3f 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -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.LogsNotifyMessage) { +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.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)) } } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 6bac6aa5509c4..672518019a84c 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -581,7 +581,7 @@ type PatchLogs struct { // PatchLogs writes log messages to the agent startup script. // Log messages are limited to 1MB in total. func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error { - res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/startup-logs", req) + res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/logs", req) if err != nil { return err } diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 2b5dba40fd545..0776764bbf9f3 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -557,7 +557,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 []WorkspaceAgentLog, 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)) @@ -569,7 +569,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 } diff --git a/docs/admin/automation.md b/docs/admin/automation.md index 7cfe563b9147d..2b0d91a2d047c 100644 --- a/docs/admin/automation.md +++ b/docs/admin/automation.md @@ -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\": [ diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ba412e39e8764..3c6cc29e26c0d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -775,11 +775,11 @@ export const getWorkspaceBuildLogs = async ( return response.data } -export const getWorkspaceAgentStartupLogs = async ( +export const getWorkspaceAgentLogs = async ( agentID: string, -): Promise => { - const response = await axios.get( - `/api/v2/workspaceagents/${agentID}/startup-logs`, +): Promise => { + const response = await axios.get( + `/api/v2/workspaceagents/${agentID}/logs`, ) return response.data } @@ -1272,21 +1272,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 couldn’t be completed. Protocol error + // WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error // const noCompression = userAgentParser(navigator.userAgent).browser.name === "Safari" @@ -1295,11 +1295,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", () => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 4b830d547cca0..0fb5a883bf182 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -547,8 +547,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", @@ -1781,7 +1781,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", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 38d3ca6b7c262..27418b2c4ae9c 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -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)) }), ] diff --git a/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts b/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts index 188f628ff7605..6f47131d7fc06 100644 --- a/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts +++ b/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts @@ -75,7 +75,7 @@ export const workspaceAgentLogsMachine = createMachine( { services: { getStartupLogs: (ctx) => - API.getWorkspaceAgentStartupLogs(ctx.agentID).then((data) => + API.getWorkspaceAgentLogs(ctx.agentID).then((data) => data.map((log) => ({ id: log.id, level: log.level || "info", @@ -89,7 +89,7 @@ export const workspaceAgentLogsMachine = createMachine( after = ctx.startupLogs[ctx.startupLogs.length - 1].id } - const socket = API.watchStartupLogs(ctx.agentID, { + const socket = API.watchWorkspaceAgentLogs(ctx.agentID, { after, onMessage: (logs) => { callback({ diff --git a/site/vite.config.ts b/site/vite.config.ts index 6e15f7e0f9531..8ba6321d17d44 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -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") { From ae89b12ac0cff797ae138d78f1163dbccd78a450 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 21 Jul 2023 17:26:42 +0000 Subject: [PATCH 04/13] Rename the frontend --- coderd/apidoc/docs.go | 101 ++++++++++++++ coderd/apidoc/swagger.json | 91 +++++++++++++ docs/api/agents.md | 128 ++++++++++++++++++ site/src/components/Resources/AgentRow.tsx | 41 +++--- .../WorkspacePage/WorkspacePage.test.tsx | 10 +- .../workspaceAgentLogsXService.ts | 54 ++++---- 6 files changed, 373 insertions(+), 52 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4f3e1128cd88d..91d14c8eaa99f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4680,6 +4680,45 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/startup-logs": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Removed: Patch workspace agent logs", + "operationId": "removed-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/{workspaceagent}": { "get": { "security": [ @@ -4943,6 +4982,68 @@ const docTemplate = `{ } } }, + "/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" + } + } + } + } + } + }, "/workspaceagents/{workspaceagent}/watch-metadata": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8a79506a5be0c..c5ae47675fed0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4116,6 +4116,39 @@ } } }, + "/workspaceagents/me/startup-logs": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Removed: Patch workspace agent logs", + "operationId": "removed-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/{workspaceagent}": { "get": { "security": [ @@ -4355,6 +4388,64 @@ } } }, + "/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" + } + } + } + } + } + }, "/workspaceagents/{workspaceagent}/watch-metadata": { "get": { "security": [ diff --git a/docs/api/agents.md b/docs/api/agents.md index d82e6b16cd703..486d122d64747 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -528,6 +528,66 @@ 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). +## Removed: Patch workspace agent logs + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/startup-logs \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /workspaceagents/me/startup-logs` + +> Body parameter + +```json +{ + "logs": [ + { + "created_at": "string", + "level": "trace", + "output": "string", + "stage": "string" + } + ] +} +``` + +### 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 workspace agent by ID ### Code samples @@ -880,3 +940,71 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/pty | 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Removed: Get logs by workspace agent + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/startup-logs \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/{workspaceagent}/startup-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) | + +

Response Schema

+ +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). diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index a6a7e9fb57d86..f26c8b8fbdd5c 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -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 = ({ hideVSCodeDesktopButton, serverVersion, onUpdateAgent, - storybookStartupLogs, + storybookLogs, storybookAgentMetadata, sshPrefix, }) => { @@ -75,10 +75,10 @@ export const AgentRow: FC = ({ 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 = ({ ((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(null) const logListDivRef = useRef(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 = ({ }) } 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 = ({ 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 = ({ {hasStartupFeatures && (
- + {({ width }) => ( = ({
- {showStartupLogs ? ( + {showLogs ? (