feat(site): add agent connection timings (#15276)

Local preview:

<img width="1260" alt="Screenshot 2024-10-29 at 16 16 01"
src="https://github.com/user-attachments/assets/10fdb20d-1f2a-4b0a-a8a1-171050ee620d">


Close https://github.com/coder/internal/issues/116

---------

Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Bruno Quaresma
2024-11-01 13:29:00 -03:00
committed by GitHub
co-authored by Danny Kopping
parent 18ef954a03
commit e232aee011
22 changed files with 747 additions and 304 deletions
+60 -2
View File
@@ -9020,6 +9020,28 @@ const docTemplate = `{
}
}
},
"codersdk.AgentConnectionTiming": {
"type": "object",
"properties": {
"ended_at": {
"type": "string",
"format": "date-time"
},
"stage": {
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"workspace_agent_id": {
"type": "string"
},
"workspace_agent_name": {
"type": "string"
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
@@ -9034,7 +9056,7 @@ const docTemplate = `{
"type": "integer"
},
"stage": {
"type": "string"
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
@@ -9042,6 +9064,12 @@ const docTemplate = `{
},
"status": {
"type": "string"
},
"workspace_agent_id": {
"type": "string"
},
"workspace_agent_name": {
"type": "string"
}
}
},
@@ -12170,7 +12198,7 @@ const docTemplate = `{
"type": "string"
},
"stage": {
"type": "string"
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
@@ -13473,6 +13501,29 @@ const docTemplate = `{
"TemplateVersionWarningUnsupportedWorkspaces"
]
},
"codersdk.TimingStage": {
"type": "string",
"enum": [
"init",
"plan",
"graph",
"apply",
"start",
"stop",
"cron",
"connect"
],
"x-enum-varnames": [
"TimingStageInit",
"TimingStagePlan",
"TimingStageGraph",
"TimingStageApply",
"TimingStageStart",
"TimingStageStop",
"TimingStageCron",
"TimingStageConnect"
]
},
"codersdk.TokenConfig": {
"type": "object",
"properties": {
@@ -14806,7 +14857,14 @@ const docTemplate = `{
"codersdk.WorkspaceBuildTimings": {
"type": "object",
"properties": {
"agent_connection_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentConnectionTiming"
}
},
"agent_script_timings": {
"description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentScriptTiming"
+60 -2
View File
@@ -7992,6 +7992,28 @@
}
}
},
"codersdk.AgentConnectionTiming": {
"type": "object",
"properties": {
"ended_at": {
"type": "string",
"format": "date-time"
},
"stage": {
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"workspace_agent_id": {
"type": "string"
},
"workspace_agent_name": {
"type": "string"
}
}
},
"codersdk.AgentScriptTiming": {
"type": "object",
"properties": {
@@ -8006,7 +8028,7 @@
"type": "integer"
},
"stage": {
"type": "string"
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
@@ -8014,6 +8036,12 @@
},
"status": {
"type": "string"
},
"workspace_agent_id": {
"type": "string"
},
"workspace_agent_name": {
"type": "string"
}
}
},
@@ -10986,7 +11014,7 @@
"type": "string"
},
"stage": {
"type": "string"
"$ref": "#/definitions/codersdk.TimingStage"
},
"started_at": {
"type": "string",
@@ -12228,6 +12256,29 @@
"enum": ["UNSUPPORTED_WORKSPACES"],
"x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"]
},
"codersdk.TimingStage": {
"type": "string",
"enum": [
"init",
"plan",
"graph",
"apply",
"start",
"stop",
"cron",
"connect"
],
"x-enum-varnames": [
"TimingStageInit",
"TimingStagePlan",
"TimingStageGraph",
"TimingStageApply",
"TimingStageStart",
"TimingStageStop",
"TimingStageCron",
"TimingStageConnect"
]
},
"codersdk.TokenConfig": {
"type": "object",
"properties": {
@@ -13475,7 +13526,14 @@
"codersdk.WorkspaceBuildTimings": {
"type": "object",
"properties": {
"agent_connection_timings": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentConnectionTiming"
}
},
"agent_script_timings": {
"description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AgentScriptTiming"
+9 -7
View File
@@ -2894,13 +2894,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)
+23 -7
View File
@@ -5899,15 +5899,31 @@ 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 {
if a.ID == script.WorkspaceAgentID {
agent = a
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,
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
+16 -8
View File
@@ -11454,7 +11454,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
@@ -11464,13 +11468,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) {
@@ -11490,6 +11496,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context
&i.Stage,
&i.Status,
&i.DisplayName,
&i.WorkspaceAgentID,
&i.WorkspaceAgentName,
); err != nil {
return nil, err
}
+5 -1
View File
@@ -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
+34 -9
View File
@@ -957,15 +957,29 @@ 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 {
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
JobID: t.JobID,
Stage: string(t.Stage),
Stage: codersdk.TimingStage(t.Stage),
Source: t.Source,
Action: t.Action,
Resource: t.Resource,
@@ -975,12 +989,23 @@ 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: codersdk.TimingStage(t.Stage),
Status: string(t.Status),
DisplayName: t.DisplayName,
WorkspaceAgentID: t.WorkspaceAgentID.String(),
WorkspaceAgentName: t.WorkspaceAgentName,
})
}
for _, agent := range agents {
res.AgentConnectionTimings = append(res.AgentConnectionTimings, codersdk.AgentConnectionTiming{
WorkspaceAgentID: agent.ID.String(),
WorkspaceAgentName: agent.Name,
StartedAt: agent.CreatedAt,
Stage: codersdk.TimingStageConnect,
EndedAt: agent.FirstConnectedAt.Time,
})
}
+95 -22
View File
@@ -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,10 +1238,13 @@ func TestWorkspaceBuildTimings(t *testing.T) {
})
}
//nolint:paralleltest
t.Run("NonExistentBuild", func(t *testing.T) {
// When: fetching an inexistent build
t.Parallel()
// Given: a non-existent 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)
@@ -1250,10 +1254,13 @@ func TestWorkspaceBuildTimings(t *testing.T) {
require.Contains(t, err.Error(), "not found")
})
//nolint:paralleltest
t.Run("EmptyTimings", func(t *testing.T) {
// When: fetching timings for a build with no timings
t.Parallel()
// 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)
@@ -1264,25 +1271,27 @@ func TestWorkspaceBuildTimings(t *testing.T) {
require.Empty(t, res.AgentScriptTimings)
})
//nolint:paralleltest
t.Run("ProvisionerTimings", func(t *testing.T) {
// When: fetching timings for a build with provisioner timings
t.Parallel()
// 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]
require.Equal(t, genTiming.Resource, timingRes.Resource)
require.Equal(t, genTiming.Action, timingRes.Action)
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
require.Equal(t, string(genTiming.Stage), string(timingRes.Stage))
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
require.Equal(t, genTiming.Source, timingRes.Source)
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
@@ -1290,9 +1299,10 @@ func TestWorkspaceBuildTimings(t *testing.T) {
}
})
//nolint:paralleltest
t.Run("AgentScriptTimings", func(t *testing.T) {
// When: fetching timings for a build with agent script timings
t.Parallel()
// Given: a build with agent script timings
build := makeBuild()
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
@@ -1305,27 +1315,31 @@ 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]
require.Equal(t, genTiming.ExitCode, timingRes.ExitCode)
require.Equal(t, string(genTiming.Status), timingRes.Status)
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
require.Equal(t, string(genTiming.Stage), string(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)
}
})
//nolint:paralleltest
t.Run("NoAgentScripts", func(t *testing.T) {
// When: fetching timings for a build with no agent scripts
t.Parallel()
// Given: a build with no agent scripts
build := makeBuild()
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
@@ -1334,29 +1348,88 @@ 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
t.Parallel()
// 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)
require.Empty(t, res.AgentConnectionTimings)
})
t.Run("AgentConnectionTimings", func(t *testing.T) {
t.Parallel()
// Given: a build with an agent
build := makeBuild()
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
agent := 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, 1)
for i := range res.ProvisionerTimings {
timingRes := res.AgentConnectionTimings[i]
require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID)
require.Equal(t, agent.Name, timingRes.WorkspaceAgentName)
require.NotEmpty(t, timingRes.StartedAt)
require.NotEmpty(t, timingRes.EndedAt)
}
})
t.Run("MultipleAgents", func(t *testing.T) {
t.Parallel()
// 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)
})
}