From 8f15ca13e1c0df38202425a3cb5dc4d9a92e138d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 25 Oct 2024 16:14:06 +0000 Subject: [PATCH 01/17] Add agent ID and name on agent script timing response --- coderd/apidoc/docs.go | 6 +++++ coderd/apidoc/swagger.json | 6 +++++ coderd/database/dbauthz/dbauthz_test.go | 16 +++++++------ coderd/database/dbmem/dbmem.go | 24 +++++++++++++------ coderd/database/queries.sql.go | 24 ++++++++++++------- coderd/database/queries/workspaceagents.sql | 6 ++++- coderd/workspacebuilds.go | 14 ++++++----- coderd/workspacebuilds_test.go | 2 ++ codersdk/workspacebuilds.go | 14 ++++++----- docs/reference/api/builds.md | 4 +++- docs/reference/api/schemas.md | 26 +++++++++++++-------- docs/reference/api/workspaces.md | 4 +++- site/src/api/typesGenerated.ts | 2 ++ 13 files changed, 101 insertions(+), 47 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 76084b1ff54dd..77c93906ae846 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8997,6 +8997,12 @@ const docTemplate = `{ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index beff69ca22373..c0c96e800b2d1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7975,6 +7975,12 @@ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 6a34e88104ce1..d372de3031e53 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2873,13 +2873,15 @@ func (s *MethodTestSuite) TestSystemFunctions() { }) rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ { - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Stage: timing.Stage, - ScriptID: timing.ScriptID, - ExitCode: timing.ExitCode, - Status: timing.Status, - DisplayName: script.DisplayName, + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Stage: timing.Stage, + ScriptID: timing.ScriptID, + ExitCode: timing.ExitCode, + Status: timing.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }, } check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 24498d88c9dbc..6122298f1e87f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5893,14 +5893,24 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex } } + var agent database.WorkspaceAgent + for _, a := range agents { + if a.ID == script.WorkspaceAgentID { + agent = a + break + } + } + rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ - ScriptID: t.ScriptID, - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: t.Stage, - Status: t.Status, - DisplayName: script.DisplayName, + ScriptID: t.ScriptID, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: t.Stage, + Status: t.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }) } return rows, nil diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 45cbef3f5e1d8..ab44b7b14de63 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11444,7 +11444,11 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks } const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id @@ -11454,13 +11458,15 @@ WHERE workspace_builds.id = $1 ` type GetWorkspaceAgentScriptTimingsByBuildIDRow struct { - ScriptID uuid.UUID `db:"script_id" json:"script_id"` - StartedAt time.Time `db:"started_at" json:"started_at"` - EndedAt time.Time `db:"ended_at" json:"ended_at"` - ExitCode int32 `db:"exit_code" json:"exit_code"` - Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` - Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` - DisplayName string `db:"display_name" json:"display_name"` + ScriptID uuid.UUID `db:"script_id" json:"script_id"` + StartedAt time.Time `db:"started_at" json:"started_at"` + EndedAt time.Time `db:"ended_at" json:"ended_at"` + ExitCode int32 `db:"exit_code" json:"exit_code"` + Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` + Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` + DisplayName string `db:"display_name" json:"display_name"` + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"` } func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { @@ -11480,6 +11486,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context &i.Stage, &i.Status, &i.DisplayName, + &i.WorkspaceAgentID, + &i.WorkspaceAgentName, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 2c26740db1d88..df7c829861cb2 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -303,7 +303,11 @@ VALUES RETURNING workspace_agent_script_timings.*; -- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.*, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.*, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 3515bc4a944b5..b97030400e525 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1002,12 +1002,14 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) } for _, t := range agentScriptTimings { res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{ - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: string(t.Stage), - Status: string(t.Status), - DisplayName: t.DisplayName, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: string(t.Stage), + Status: string(t.Status), + DisplayName: t.DisplayName, + WorkspaceAgentID: t.WorkspaceAgentID.String(), + WorkspaceAgentName: t.WorkspaceAgentName, }) } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index e8eeca0f49d66..79bb162ffeea7 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1320,6 +1320,8 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Equal(t, string(genTiming.Stage), timingRes.Stage) require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) + require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agent.Name, timingRes.WorkspaceAgentName) } }) diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 3cb00c313f4bf..8e4a4059762e6 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -186,12 +186,14 @@ type ProvisionerTiming struct { } type AgentScriptTiming struct { - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` - ExitCode int32 `json:"exit_code"` - Stage string `json:"stage"` - Status string `json:"status"` - DisplayName string `json:"display_name"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + ExitCode int32 `json:"exit_code"` + Stage string `json:"stage"` + Status string `json:"status"` + DisplayName string `json:"display_name"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` } type WorkspaceBuildTimings struct { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index d49ab50fbb1ef..9fc638a4164d6 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1023,7 +1023,9 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim "exit_code": 0, "stage": "string", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ed3800b3a27cd..9e49f5adbd523 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -358,20 +358,24 @@ "exit_code": 0, "stage": "string", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | ----------- | -| `display_name` | string | false | | | -| `ended_at` | string | false | | | -| `exit_code` | integer | false | | | -| `stage` | string | false | | | -| `started_at` | string | false | | | -| `status` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------- | -------- | ------------ | ----------- | +| `display_name` | string | false | | | +| `ended_at` | string | false | | | +| `exit_code` | integer | false | | | +| `stage` | string | false | | | +| `started_at` | string | false | | | +| `status` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | ## codersdk.AgentSubsystem @@ -7384,7 +7388,9 @@ If the schedule is empty, the user will be updated to use the default schedule.| "exit_code": 0, "stage": "string", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 283dab5db91b5..591d6b0dfe498 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1648,7 +1648,9 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "exit_code": 0, "stage": "string", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e55167ef03f88..3eb25452d9e48 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -40,6 +40,8 @@ export interface AgentScriptTiming { readonly stage: string; readonly status: string; readonly display_name: string; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; } // From codersdk/templates.go From 8ac895fa0ce6023d70ea40f944832be054334c56 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 25 Oct 2024 17:05:26 +0000 Subject: [PATCH 02/17] Add agent connection timings to the response --- coderd/apidoc/docs.go | 25 +++++++ coderd/apidoc/swagger.json | 25 +++++++ coderd/workspacebuilds.go | 26 +++++++- coderd/workspacebuilds_test.go | 66 +++++++++++++++---- codersdk/workspacebuilds.go | 12 +++- docs/reference/api/builds.md | 8 +++ docs/reference/api/schemas.md | 37 +++++++++-- docs/reference/api/workspaces.md | 8 +++ site/src/api/typesGenerated.ts | 9 +++ .../WorkspaceTiming/storybookData.ts | 17 +++++ 10 files changed, 212 insertions(+), 21 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 77c93906ae846..0f2a79d0f92ff 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8975,6 +8975,25 @@ const docTemplate = `{ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -14755,6 +14774,12 @@ const docTemplate = `{ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c0c96e800b2d1..ab53bb5398d56 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7953,6 +7953,25 @@ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -13426,6 +13445,12 @@ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { "type": "array", "items": { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index b97030400e525..5aeafd8b36484 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -984,9 +984,23 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err) } + resources, err := api.Database.GetWorkspaceResourcesByJobID(ctx, build.JobID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace resources: %w", err) + } + resourceIDs := make([]uuid.UUID, 0, len(resources)) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agents: %w", err) + } + res := codersdk.WorkspaceBuildTimings{ - ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), - AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), + AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + AgentConnectionTimings: make([]codersdk.AgentConnectionTiming, 0, len(agents)), } for _, t := range provisionerTimings { @@ -1012,6 +1026,14 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) WorkspaceAgentName: t.WorkspaceAgentName, }) } + for _, agent := range agents { + res.AgentConnectionTimings = append(res.AgentConnectionTimings, codersdk.AgentConnectionTiming{ + WorkspaceAgentID: agent.ID.String(), + WorkspaceAgentName: agent.Name, + StartedAt: agent.CreatedAt, + EndedAt: agent.FirstConnectedAt.Time, + }) + } return res, nil } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 79bb162ffeea7..0b6fe2565c33c 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1239,8 +1239,10 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("NonExistentBuild", func(t *testing.T) { - // When: fetching an inexistent build + // Given: an inexistent build buildID := uuid.New() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) _, err := client.WorkspaceBuildTimings(ctx, buildID) @@ -1252,8 +1254,10 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("EmptyTimings", func(t *testing.T) { - // When: fetching timings for a build with no timings + // Given: a build with no timings build := makeBuild() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) @@ -1266,17 +1270,18 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("ProvisionerTimings", func(t *testing.T) { - // When: fetching timings for a build with provisioner timings + // Given: a build with provisioner timings build := makeBuild() provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.ProvisionerTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.ProvisionerTimings, 5) for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] genTiming := provisionerTimings[i] @@ -1292,7 +1297,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("AgentScriptTimings", func(t *testing.T) { - // When: fetching timings for a build with agent script timings + // Given: a build with agent script timings build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1305,13 +1310,14 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.AgentScriptTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.AgentScriptTimings, 5) for i := range res.AgentScriptTimings { timingRes := res.AgentScriptTimings[i] genTiming := agentScriptTimings[i] @@ -1327,7 +1333,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("NoAgentScripts", func(t *testing.T) { - // When: fetching timings for a build with no agent scripts + // Given: a build with no agent scripts build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1336,29 +1342,63 @@ func TestWorkspaceBuildTimings(t *testing.T) { ResourceID: resource.ID, }) - // Then: return a response with empty agent script timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) }) // Some workspaces might not have agents. It is improbable, but possible. //nolint:paralleltest t.Run("NoAgents", func(t *testing.T) { - // When: fetching timings for a build with no agents + // Given: a build with no agents build := makeBuild() dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, }) - // Then: return a response with empty agent script timings - // trigger build + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) }) + + //nolint:paralleltest + t.Run("AgentConnectionTimings", func(t *testing.T) { + // Given: a build with multiple agents + build := makeBuild() + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agents := make([]database.WorkspaceAgent, 5) + for i := range agents { + agents[i] = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + } + + // When: fetching timings for the build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) + require.NoError(t, err) + + // Then: return a response with the expected timings + require.Len(t, res.AgentConnectionTimings, 5) + for i := range res.ProvisionerTimings { + timingRes := res.AgentConnectionTimings[i] + require.Equal(t, agents[i].ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agents[i].Name, timingRes.WorkspaceAgentName) + require.NotEmpty(t, timingRes.StartedAt) + require.NotEmpty(t, timingRes.EndedAt) + } + }) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 8e4a4059762e6..9c07ea6aeebfe 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -196,9 +196,17 @@ type AgentScriptTiming struct { WorkspaceAgentName string `json:"workspace_agent_name"` } +type AgentConnectionTiming struct { + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` +} + type WorkspaceBuildTimings struct { - ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` - AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` + AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + AgentConnectionTimings []AgentConnectionTiming `json:"agent_connection_timings"` } func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (WorkspaceBuildTimings, error) { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 9fc638a4164d6..637dec12b5c5c 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1016,6 +1016,14 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 9e49f5adbd523..dac3ec0440435 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -349,6 +349,26 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | +## codersdk.AgentConnectionTiming + +```json +{ + "ended_at": "2019-08-24T14:15:22Z", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------ | -------- | ------------ | ----------- | +| `ended_at` | string | false | | | +| `started_at` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | + ## codersdk.AgentScriptTiming ```json @@ -7381,6 +7401,14 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", @@ -7409,10 +7437,11 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | -| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | | -| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------------- | ------------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `agent_connection_timings` | array of [codersdk.AgentConnectionTiming](#codersdkagentconnectiontiming) | false | | | +| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | | +| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | ## codersdk.WorkspaceConnectionLatencyMS diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 591d6b0dfe498..a963e9dffc49e 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1641,6 +1641,14 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3eb25452d9e48..78ea8b0881948 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,6 +32,14 @@ export interface AddLicenseRequest { readonly license: string; } +// From codersdk/workspacebuilds.go +export interface AgentConnectionTiming { + readonly started_at: string; + readonly ended_at: string; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; +} + // From codersdk/workspacebuilds.go export interface AgentScriptTiming { readonly started_at: string; @@ -1987,6 +1995,7 @@ export interface WorkspaceBuildParameter { export interface WorkspaceBuildTimings { readonly provisioner_timings: Readonly>; readonly agent_script_timings: Readonly>; + readonly agent_connection_timings: Readonly>; } // From codersdk/workspaces.go diff --git a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts index 828959f424107..960f4d3584f0c 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts @@ -355,6 +355,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Startup Script", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650915Z", @@ -363,6 +365,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Dotfiles", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650715Z", @@ -371,6 +375,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Personalize", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650512Z", @@ -379,6 +385,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "install_slackme", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650659Z", @@ -387,6 +395,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Coder Login", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650666Z", @@ -395,6 +405,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "File Browser", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.652425Z", @@ -403,6 +415,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "code-server", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650423Z", @@ -411,6 +425,9 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Git Clone", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, ], + agent_connection_timings: [], }; From 124d14928e947d9ad63252ffb19b800082557403 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 29 Oct 2024 11:54:20 +0000 Subject: [PATCH 03/17] Remove unused modules --- .../src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx index b1c69b6d1baf7..e127971387243 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx @@ -1,8 +1,4 @@ -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import { type FC, useState } from "react"; -import { Link } from "react-router-dom"; import { Bar } from "./Chart/Bar"; import { Chart, From d00a00be2de862cbb1ede1292b4e1b04614ac1b8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 29 Oct 2024 20:01:54 +0000 Subject: [PATCH 04/17] Display agent timings --- site/src/api/queries/workspaceBuilds.ts | 5 +- site/src/api/typesGenerated.ts | 1 + .../WorkspaceTiming/ResourcesChart.tsx | 13 +- .../WorkspaceTiming/ScriptsChart.tsx | 12 +- .../WorkspaceTiming/StagesChart.tsx | 260 +++++++++--------- .../WorkspaceTimings.stories.tsx | 1 + .../WorkspaceTiming/WorkspaceTimings.tsx | 69 +++-- .../WorkspaceTiming/storybookData.ts | 10 +- site/src/pages/WorkspacePage/Workspace.tsx | 3 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 5 +- 10 files changed, 213 insertions(+), 166 deletions(-) diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 0e8981ba71ea4..72a10487596df 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -57,9 +57,10 @@ export const infiniteWorkspaceBuilds = ( }; }; -export const workspaceBuildTimings = (workspaceBuildId: string) => { +// We use readyAgentsCount to invalidate the query when an agent connects +export const workspaceBuildTimings = (workspaceBuildId: string, readyAgentsCount: number) => { return { - queryKey: ["workspaceBuilds", workspaceBuildId, "timings"], + queryKey: ["workspaceBuilds", workspaceBuildId, "timings", { readyAgentsCount }], queryFn: () => API.workspaceBuildTimings(workspaceBuildId), }; }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2539a2dfec69a..ff8da7746b077 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -36,6 +36,7 @@ export interface AddLicenseRequest { export interface AgentConnectionTiming { readonly started_at: string; readonly ended_at: string; + readonly stage: string; readonly workspace_agent_id: string; readonly workspace_agent_name: string; } diff --git a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx index a5c3d0e3b3a03..bcbf2b02937a1 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx @@ -26,7 +26,8 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import { type Theme, useTheme } from "@emotion/react"; +import type { Stage } from "./StagesChart"; type ResourceTiming = { name: string; @@ -36,14 +37,12 @@ type ResourceTiming = { }; export type ResourcesChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ResourceTiming[]; onBack: () => void; }; export const ResourcesChart: FC = ({ - category, stage, timings, onBack, @@ -67,11 +66,11 @@ export const ResourcesChart: FC = ({ @@ -85,7 +84,7 @@ export const ResourcesChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx index 64d97bff7cfdb..3824913a87b43 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx @@ -27,7 +27,7 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import type { Stage } from "./StagesChart"; type ScriptTiming = { name: string; @@ -37,14 +37,12 @@ type ScriptTiming = { }; export type ScriptsChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ScriptTiming[]; onBack: () => void; }; export const ScriptsChart: FC = ({ - category, stage, timings, onBack, @@ -66,11 +64,11 @@ export const ScriptsChart: FC = ({ @@ -84,7 +82,7 @@ export const ScriptsChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index dc5550dcfed98..066fff5c0001b 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -28,118 +28,34 @@ import { mergeTimeRanges, } from "./Chart/utils"; -export type StageCategory = { - name: string; - id: "provisioning" | "workspaceBoot"; -}; - -const stageCategories: StageCategory[] = [ - { - name: "provisioning", - id: "provisioning", - }, - { - name: "workspace boot", - id: "workspaceBoot", - }, -] as const; - export type Stage = { + /** + * The name is used to identify the stage. + */ name: string; - categoryID: StageCategory["id"]; + /** + * The value to display in the stage label. This can differ from the stage + * name to provide more context or clarity. + */ + label: string; + /** + * The section is used to group stages together. + */ + section: string; + /** + * The tooltip is used to provide additional information about the stage. + */ tooltip: Omit; }; -export const stages: Stage[] = [ - { - name: "init", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform initialization - - Download providers & modules. - - - ), - }, - }, - { - name: "plan", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform plan - - Compare state of desired vs actual resources and compute changes to - be made. - - - ), - }, - }, - { - name: "graph", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform graph - - List all resources in plan, used to update coderd database. - - - ), - }, - }, - { - name: "apply", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform apply - - Execute Terraform plan to create/modify/delete resources into - desired states. - - - ), - }, - }, - { - name: "start", - categoryID: "workspaceBoot", - tooltip: { - title: ( - <> - Start - - Scripts executed when the agent is starting. - - - ), - }, - }, -]; - type StageTiming = { - name: string; - /** + stage: Stage; /** * Represents the number of resources included in this stage that can be * inspected. This value is used to display individual blocks within the bar, * indicating that the stage consists of multiple resource time blocks. */ visibleResources: number; - /** - * Represents the category of the stage. This value is used to group stages - * together in the chart. For example, all provisioning stages are grouped - * together. - */ - categoryID: StageCategory["id"]; /** * Represents the time range of the stage. This value is used to calculate the * duration of the stage and to position the stage within the chart. This can @@ -155,7 +71,7 @@ type StageTiming = { export type StagesChartProps = { timings: StageTiming[]; - onSelectStage: (timing: StageTiming, category: StageCategory) => void; + onSelectStage: (stage: Stage) => void; }; export const StagesChart: FC = ({ @@ -167,28 +83,26 @@ export const StagesChart: FC = ({ ); const totalTime = calcDuration(totalRange); const [ticks, scale] = makeTicks(totalTime); + const sections = Array.from(new Set(timings.map((t) => t.stage.section))); return ( - {stageCategories.map((c) => { - const stagesInCategory = stages.filter( - (s) => s.categoryID === c.id, - ); + {sections.map((section) => { + const stages = timings + .filter((t) => t.stage.section === section) + .map((t) => t.stage); return ( - - {c.name} + + {section} - {stagesInCategory.map((stage) => ( - + {stages.map((s) => ( + - {stage.name} - + {s.label} + @@ -201,19 +115,19 @@ export const StagesChart: FC = ({ - {stageCategories.map((category) => { + {sections.map((section) => { const stageTimings = timings.filter( - (t) => t.categoryID === category.id, + (t) => t.stage.section === section, ); return ( - + {stageTimings.map((t) => { // If the stage has no timing data, we just want to render an empty row if (t.range === undefined) { return ( ); } @@ -223,18 +137,18 @@ export const StagesChart: FC = ({ return ( {/** We only want to expand stages with more than one resource */} {t.visibleResources > 1 ? ( { - onSelectStage(t, category); + onSelectStage(t.stage); }} > {t.error && ( @@ -281,3 +195,103 @@ const styles = { cursor: "pointer", }), } satisfies Record>; + +export const provisioningStages: Stage[] = [ + { + name: "init", + label: "init", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform initialization + + Download providers & modules. + + + ), + }, + }, + { + name: "plan", + label: "plan", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform plan + + Compare state of desired vs actual resources and compute changes to + be made. + + + ), + }, + }, + { + name: "graph", + label: "graph", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform graph + + List all resources in plan, used to update coderd database. + + + ), + }, + }, + { + name: "apply", + label: "apply", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform apply + + Execute Terraform plan to create/modify/delete resources into + desired states. + + + ), + }, + }, +]; + +export const agentStages = (section: string): Stage[] => { + return [ + { + name: "connect", + label: "connect", + section, + tooltip: { + title: ( + <> + Connect + + Time taken to establish an RPC connection with the control plane. + + + ), + }, + }, + { + name: "start", + label: "run startup scripts", + section, + tooltip: { + title: ( + <> + Run startup scripts + + Scripts executed when the agent is starting. + + + ), + }, + }, + ]; +}; diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index f546e271395ab..02fd64d11d689 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -11,6 +11,7 @@ const meta: Meta = { defaultIsOpen: true, provisionerTimings: WorkspaceTimingsResponse.provisioner_timings, agentScriptTimings: WorkspaceTimingsResponse.agent_script_timings, + agentConnectionTimings: WorkspaceTimingsResponse.agent_connection_timings, }, parameters: { chromatic, diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 9e16e55bae36e..2954847913f33 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -4,19 +4,27 @@ import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp"; import Button from "@mui/material/Button"; import Collapse from "@mui/material/Collapse"; import Skeleton from "@mui/material/Skeleton"; -import type { AgentScriptTiming, ProvisionerTiming } from "api/typesGenerated"; +import type { + AgentConnectionTiming, + AgentScriptTiming, + ProvisionerTiming, +} from "api/typesGenerated"; import { type FC, useState } from "react"; import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils"; import { ResourcesChart, isCoderResource } from "./ResourcesChart"; import { ScriptsChart } from "./ScriptsChart"; -import { type StageCategory, StagesChart, stages } from "./StagesChart"; +import { + agentStages, + provisioningStages, + type Stage, + StagesChart, +} from "./StagesChart"; type TimingView = | { name: "default" } | { name: "detailed"; - stage: string; - category: StageCategory; + stage: Stage; filter: string; }; @@ -24,24 +32,40 @@ type WorkspaceTimingsProps = { defaultIsOpen?: boolean; provisionerTimings: readonly ProvisionerTiming[] | undefined; agentScriptTimings: readonly AgentScriptTiming[] | undefined; + agentConnectionTimings: readonly AgentConnectionTiming[] | undefined; }; export const WorkspaceTimings: FC = ({ provisionerTimings = [], agentScriptTimings = [], + agentConnectionTimings = [], defaultIsOpen = false, }) => { const [view, setView] = useState({ name: "default" }); - const timings = [...provisionerTimings, ...agentScriptTimings]; + const timings = [ + ...provisionerTimings, + ...agentScriptTimings, + ...agentConnectionTimings, + ]; const [isOpen, setIsOpen] = useState(defaultIsOpen); const isLoading = timings.length === 0; const displayProvisioningTime = () => { - const totalRange = mergeTimeRanges(timings.map(extractRange)); + const totalRange = mergeTimeRanges(timings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); return humanizeDuration(totalDuration); }; + const stages = () => { + const agentNames = Array.from( + new Set(agentConnectionTimings.map((t) => t.workspace_agent_name)), + ); + return [ + ...provisioningStages, + ...agentNames.flatMap((a) => agentStages(a)), + ]; + }; + return (
diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 6859a5ada7882..cdb47f86f508c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC = ({ const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); // Build Timings. Fetch build timings only when the build job is completed. + const readyAgents = workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => a && a.lifecycle_state !== "starting"); const timingsQuery = useQuery({ - ...workspaceBuildTimings(workspace.latest_build.id), + ...workspaceBuildTimings(workspace.latest_build.id, readyAgents.length), enabled: Boolean(workspace.latest_build.job.completed_at), }); From 69b181c9b16f9f9f71f74bc73fe86cec2dfff407 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 29 Oct 2024 20:02:16 +0000 Subject: [PATCH 05/17] Display agent connection timings --- .vscode/settings.json | 3 +++ coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/workspacebuilds.go | 1 + codersdk/workspacebuilds.go | 1 + docs/reference/api/builds.md | 1 + docs/reference/api/schemas.md | 3 +++ docs/reference/api/workspaces.md | 1 + 8 files changed, 16 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6695a12faa8dc..009e0b9cf6c58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -257,5 +257,8 @@ "[css][html][markdown][yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7cd02043e9c81..1fbed33ed1156 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9008,6 +9008,9 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "stage": { + "type": "string" + }, "started_at": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d67f58061d707..ea1bd9076a5a1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7982,6 +7982,9 @@ "type": "string", "format": "date-time" }, + "stage": { + "type": "string" + }, "started_at": { "type": "string", "format": "date-time" diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 5aeafd8b36484..7e61aab9dffa0 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1031,6 +1031,7 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) WorkspaceAgentID: agent.ID.String(), WorkspaceAgentName: agent.Name, StartedAt: agent.CreatedAt, + Stage: "connect", EndedAt: agent.FirstConnectedAt.Time, }) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 9c07ea6aeebfe..9738f30b29f67 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -199,6 +199,7 @@ type AgentScriptTiming struct { type AgentConnectionTiming struct { StartedAt time.Time `json:"started_at" format:"date-time"` EndedAt time.Time `json:"ended_at" format:"date-time"` + Stage string `json:"stage"` WorkspaceAgentID string `json:"workspace_agent_id"` WorkspaceAgentName string `json:"workspace_agent_name"` } diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 637dec12b5c5c..3b9697f178a9f 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1019,6 +1019,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim "agent_connection_timings": [ { "ended_at": "2019-08-24T14:15:22Z", + "stage": "string", "started_at": "2019-08-24T14:15:22Z", "workspace_agent_id": "string", "workspace_agent_name": "string" diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index bff966c9ec041..6fe78a2082982 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -354,6 +354,7 @@ ```json { "ended_at": "2019-08-24T14:15:22Z", + "stage": "string", "started_at": "2019-08-24T14:15:22Z", "workspace_agent_id": "string", "workspace_agent_name": "string" @@ -365,6 +366,7 @@ | Name | Type | Required | Restrictions | Description | | ---------------------- | ------ | -------- | ------------ | ----------- | | `ended_at` | string | false | | | +| `stage` | string | false | | | | `started_at` | string | false | | | | `workspace_agent_id` | string | false | | | | `workspace_agent_name` | string | false | | | @@ -7405,6 +7407,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "agent_connection_timings": [ { "ended_at": "2019-08-24T14:15:22Z", + "stage": "string", "started_at": "2019-08-24T14:15:22Z", "workspace_agent_id": "string", "workspace_agent_name": "string" diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index a963e9dffc49e..c046364476d9e 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1644,6 +1644,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "agent_connection_timings": [ { "ended_at": "2019-08-24T14:15:22Z", + "stage": "string", "started_at": "2019-08-24T14:15:22Z", "workspace_agent_id": "string", "workspace_agent_name": "string" From 3e1170c1730cea1d3cf01d1c8ca854584b2f0f4d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 30 Oct 2024 10:37:57 -0300 Subject: [PATCH 06/17] Apply suggestions from code review Co-authored-by: Danny Kopping --- coderd/workspacebuilds_test.go | 2 +- site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 0b6fe2565c33c..53c4742626be4 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1239,7 +1239,7 @@ func TestWorkspaceBuildTimings(t *testing.T) { //nolint:paralleltest t.Run("NonExistentBuild", func(t *testing.T) { - // Given: an inexistent build + // Given: a non-existent build buildID := uuid.New() // When: fetching timings for the build diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index 066fff5c0001b..d2ede308a6ed3 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -287,7 +287,7 @@ export const agentStages = (section: string): Stage[] => { <> Run startup scripts - Scripts executed when the agent is starting. + Time taken to run each agent startup script. ), From ab8792f005eb70a757e6395b1c1e6c22eda1f180 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 30 Oct 2024 13:56:22 +0000 Subject: [PATCH 07/17] Fix fmt --- site/src/api/queries/workspaceBuilds.ts | 12 ++++++++++-- .../workspaces/WorkspaceTiming/ResourcesChart.tsx | 2 +- .../workspaces/WorkspaceTiming/WorkspaceTimings.tsx | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 72a10487596df..45f7ac3bb7fe6 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -58,9 +58,17 @@ export const infiniteWorkspaceBuilds = ( }; // We use readyAgentsCount to invalidate the query when an agent connects -export const workspaceBuildTimings = (workspaceBuildId: string, readyAgentsCount: number) => { +export const workspaceBuildTimings = ( + workspaceBuildId: string, + readyAgentsCount: number, +) => { return { - queryKey: ["workspaceBuilds", workspaceBuildId, "timings", { readyAgentsCount }], + queryKey: [ + "workspaceBuilds", + workspaceBuildId, + "timings", + { readyAgentsCount }, + ], queryFn: () => API.workspaceBuildTimings(workspaceBuildId), }; }; diff --git a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx index bcbf2b02937a1..b1c0bd89bc5fe 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx @@ -1,3 +1,4 @@ +import { type Theme, useTheme } from "@emotion/react"; import { type FC, useState } from "react"; import { Bar } from "./Chart/Bar"; import { @@ -26,7 +27,6 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import { type Theme, useTheme } from "@emotion/react"; import type { Stage } from "./StagesChart"; type ResourceTiming = { diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 2954847913f33..ad75db6ca3015 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -14,10 +14,10 @@ import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils"; import { ResourcesChart, isCoderResource } from "./ResourcesChart"; import { ScriptsChart } from "./ScriptsChart"; import { - agentStages, - provisioningStages, type Stage, StagesChart, + agentStages, + provisioningStages, } from "./StagesChart"; type TimingView = From 0b685a93d1e288d187824228e66bdd433e79654f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 30 Oct 2024 15:25:09 +0000 Subject: [PATCH 08/17] Improve tests --- coderd/workspacebuilds_test.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 53c4742626be4..243541eb35bfd 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1183,8 +1183,9 @@ func TestPostWorkspaceBuild(t *testing.T) { }) } -//nolint:paralleltest func TestWorkspaceBuildTimings(t *testing.T) { + t.Parallel() + // Setup the test environment with a template and version db, pubsub := dbtestutil.NewDB(t) client := coderdtest.New(t, &coderdtest.Options{ @@ -1237,8 +1238,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) } - //nolint:paralleltest t.Run("NonExistentBuild", func(t *testing.T) { + t.Parallel() + // Given: a non-existent build buildID := uuid.New() @@ -1252,8 +1254,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Contains(t, err.Error(), "not found") }) - //nolint:paralleltest t.Run("EmptyTimings", func(t *testing.T) { + t.Parallel() + // Given: a build with no timings build := makeBuild() @@ -1268,8 +1271,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Empty(t, res.AgentScriptTimings) }) - //nolint:paralleltest t.Run("ProvisionerTimings", func(t *testing.T) { + t.Parallel() + // Given: a build with provisioner timings build := makeBuild() provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) @@ -1295,8 +1299,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { } }) - //nolint:paralleltest t.Run("AgentScriptTimings", func(t *testing.T) { + t.Parallel() + // Given: a build with agent script timings build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ @@ -1331,8 +1336,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { } }) - //nolint:paralleltest t.Run("NoAgentScripts", func(t *testing.T) { + t.Parallel() + // Given: a build with no agent scripts build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ @@ -1353,8 +1359,9 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) // Some workspaces might not have agents. It is improbable, but possible. - //nolint:paralleltest t.Run("NoAgents", func(t *testing.T) { + t.Parallel() + // Given: a build with no agents build := makeBuild() dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ @@ -1369,10 +1376,12 @@ func TestWorkspaceBuildTimings(t *testing.T) { // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) + require.Empty(t, res.AgentConnectionTimings) }) - //nolint:paralleltest t.Run("AgentConnectionTimings", func(t *testing.T) { + t.Parallel() + // Given: a build with multiple agents build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ From d9fc45357ddc04454a30549063f2c2ba16e7256f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 31 Oct 2024 13:18:51 +0000 Subject: [PATCH 09/17] Return error if agent or script are not found --- coderd/database/dbmem/dbmem.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b462004b59ec0..15780155b73ed 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5895,6 +5895,9 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex break } } + if script.ID == uuid.Nil { + return nil, xerrors.Errorf("script with ID %s not found", t.ScriptID) + } var agent database.WorkspaceAgent for _, a := range agents { @@ -5903,6 +5906,9 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex break } } + if agent.ID == uuid.Nil { + return nil, xerrors.Errorf("agent with ID %s not found", t.ScriptID) + } rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ ScriptID: t.ScriptID, From 1bed074e950f089031d9101bae29c3d4563409bb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 31 Oct 2024 13:21:37 +0000 Subject: [PATCH 10/17] Change the agent stage label to agent () --- .../workspaces/WorkspaceTiming/WorkspaceTimings.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index ad75db6ca3015..54dd0ac6e8f11 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -57,12 +57,14 @@ export const WorkspaceTimings: FC = ({ }; const stages = () => { - const agentNames = Array.from( - new Set(agentConnectionTimings.map((t) => t.workspace_agent_name)), + const agentStageLabels = Array.from( + new Set( + agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), + ), ); return [ ...provisioningStages, - ...agentNames.flatMap((a) => agentStages(a)), + ...agentStageLabels.flatMap((a) => agentStages(a)), ]; }; From 4bd653856a9327e524f14ffb0b941a846c05930c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 31 Oct 2024 13:23:20 +0000 Subject: [PATCH 11/17] Fix storybook --- .../workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index 02fd64d11d689..0bc994959935e 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -46,7 +46,7 @@ export const ClickToOpen: Story = { play: async ({ canvasElement }) => { const user = userEvent.setup(); const canvas = within(canvasElement); - await user.click(canvas.getByRole("button")); + await user.click(canvas.getByText("Build timeline", { exact: false })); await canvas.findByText("provisioning"); }, }; @@ -59,7 +59,7 @@ export const ClickToClose: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); await canvas.findByText("provisioning"); - await user.click(canvas.getByText("Provisioning time", { exact: false })); + await user.click(canvas.getByText("Build timeline", { exact: false })); await waitFor(() => expect(canvas.getByText("workspace boot")).not.toBeVisible(), ); From f59a87decf3f363416a6730993e0b4eb25b9b1c1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 1 Nov 2024 13:22:40 +0000 Subject: [PATCH 12/17] Remove duplicate vscode config --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 009e0b9cf6c58..6695a12faa8dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -257,8 +257,5 @@ "[css][html][markdown][yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" } } From 91be0dc89b7fb3cc267335f51c673add104123cc Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 1 Nov 2024 13:34:37 +0000 Subject: [PATCH 13/17] Apply PR suggested changes --- .../WorkspaceTiming/StagesChart.tsx | 8 +- .../WorkspaceTimings.stories.tsx | 2 +- .../WorkspaceTiming/WorkspaceTimings.tsx | 95 +++++++++---------- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index d2ede308a6ed3..35fc043493cf8 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -98,11 +98,11 @@ export const StagesChart: FC = ({ {section} - {stages.map((s) => ( - + {stages.map((stage) => ( + - {s.label} - + {stage.label} + diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index 0bc994959935e..66760242fdf48 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -97,7 +97,7 @@ export const NavigateToStartStage: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); const detailsButton = canvas.getByRole("button", { - name: "View start details", + name: "View run startup scripts details", }); await user.click(detailsButton); await canvas.findByText("Startup Script"); diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 54dd0ac6e8f11..6d369ac7cd097 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -50,24 +50,23 @@ export const WorkspaceTimings: FC = ({ const [isOpen, setIsOpen] = useState(defaultIsOpen); const isLoading = timings.length === 0; + // All stages + const agentStageLabels = Array.from( + new Set( + agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), + ), + ); + const stages = [ + ...provisioningStages, + ...agentStageLabels.flatMap((a) => agentStages(a)), + ]; + const displayProvisioningTime = () => { const totalRange = mergeTimeRanges(timings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); return humanizeDuration(totalDuration); }; - const stages = () => { - const agentStageLabels = Array.from( - new Set( - agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), - ), - ); - return [ - ...provisioningStages, - ...agentStageLabels.flatMap((a) => agentStages(a)), - ]; - }; - return (