diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index e1cdc64f3812a..bdcb61505e376 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -159,6 +159,33 @@ func (q *fakeQuerier) InsertAgentStat(_ context.Context, p database.InsertAgentS return stat, nil } +func (q *fakeQuerier) GetLatestAgentStat(_ context.Context, agentID uuid.UUID) (database.AgentStat, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + found := false + latest := database.AgentStat{} + for _, agentStat := range q.agentStats { + if agentStat.AgentID != agentID { + continue + } + if !found { + latest = agentStat + found = true + continue + } + if agentStat.CreatedAt.After(latest.CreatedAt) { + latest = agentStat + found = true + continue + } + } + if !found { + return database.AgentStat{}, sql.ErrNoRows + } + return latest, nil +} + func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, templateID uuid.UUID) ([]database.GetTemplateDAUsRow, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 62d63b929f198..0b38708a2497e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -37,6 +37,7 @@ type querier interface { GetDeploymentID(ctx context.Context) (string, error) GetFileByHash(ctx context.Context, hash string) (File, error) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) + GetLatestAgentStat(ctx context.Context, agentID uuid.UUID) (AgentStat, error) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3ccc8e6a271bc..acbd6df2b46d4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -24,6 +24,25 @@ func (q *sqlQuerier) DeleteOldAgentStats(ctx context.Context) error { return err } +const getLatestAgentStat = `-- name: GetLatestAgentStat :one +SELECT id, created_at, user_id, agent_id, workspace_id, template_id, payload FROM agent_stats WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1 +` + +func (q *sqlQuerier) GetLatestAgentStat(ctx context.Context, agentID uuid.UUID) (AgentStat, error) { + row := q.db.QueryRowContext(ctx, getLatestAgentStat, agentID) + var i AgentStat + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UserID, + &i.AgentID, + &i.WorkspaceID, + &i.TemplateID, + &i.Payload, + ) + return i, err +} + const getTemplateDAUs = `-- name: GetTemplateDAUs :many select (created_at at TIME ZONE 'UTC')::date as date, diff --git a/coderd/database/queries/agentstats.sql b/coderd/database/queries/agentstats.sql index f1d960d512a5d..ddb7d04aa0a69 100644 --- a/coderd/database/queries/agentstats.sql +++ b/coderd/database/queries/agentstats.sql @@ -12,6 +12,9 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; +-- name: GetLatestAgentStat :one +SELECT * FROM agent_stats WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1; + -- name: GetTemplateDAUs :many select (created_at at TIME ZONE 'UTC')::date as date, diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index fa464e6bbfb0e..4714713e8a9cd 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -795,10 +795,22 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques } defer conn.Close(websocket.StatusAbnormalClosure, "") + var lastReport codersdk.AgentStatsReportResponse + latestStat, err := api.Database.GetLatestAgentStat(r.Context(), workspaceAgent.ID) + if err == nil { + err = json.Unmarshal(latestStat.Payload, &lastReport) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to unmarshal stat payload.", + Detail: err.Error(), + }) + return + } + } + // Allow overriding the stat interval for debugging and testing purposes. ctx := r.Context() timer := time.NewTicker(api.AgentStatsRefreshInterval) - var lastReport codersdk.AgentStatsReportResponse for { err := wsjson.Write(ctx, conn, codersdk.AgentStatsReportRequest{}) if err != nil {