mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
chore: refactor agent stats streaming (#5112)
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@ -14,14 +15,14 @@ import (
|
||||
|
||||
// activityBumpWorkspace automatically bumps the workspace's auto-off timer
|
||||
// if it is set to expire soon.
|
||||
func activityBumpWorkspace(log slog.Logger, db database.Store, workspace database.Workspace) {
|
||||
func activityBumpWorkspace(log slog.Logger, db database.Store, workspaceID uuid.UUID) {
|
||||
// We set a short timeout so if the app is under load, these
|
||||
// low priority operations fail first.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||
defer cancel()
|
||||
|
||||
err := db.InTx(func(s database.Store) error {
|
||||
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@ -65,15 +66,13 @@ func activityBumpWorkspace(log slog.Logger, db database.Store, workspace databas
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Error(
|
||||
ctx, "bump failed",
|
||||
slog.Error(err),
|
||||
slog.F("workspace_id", workspace.ID),
|
||||
)
|
||||
} else {
|
||||
log.Debug(
|
||||
ctx, "bumped deadline from activity",
|
||||
slog.F("workspace_id", workspace.ID),
|
||||
log.Error(ctx, "bump failed", slog.Error(err),
|
||||
slog.F("workspace_id", workspaceID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(ctx, "bumped deadline from activity",
|
||||
slog.F("workspace_id", workspaceID),
|
||||
)
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
@ -132,7 +132,7 @@ func New(options *Options) *API {
|
||||
options.APIRateLimit = 512
|
||||
}
|
||||
if options.AgentStatsRefreshInterval == 0 {
|
||||
options.AgentStatsRefreshInterval = 10 * time.Minute
|
||||
options.AgentStatsRefreshInterval = 5 * time.Minute
|
||||
}
|
||||
if options.MetricsCacheRefreshInterval == 0 {
|
||||
options.MetricsCacheRefreshInterval = time.Hour
|
||||
@ -493,7 +493,10 @@ func New(options *Options) *API {
|
||||
r.Get("/gitauth", api.workspaceAgentsGitAuth)
|
||||
r.Get("/gitsshkey", api.agentGitSSHKey)
|
||||
r.Get("/coordinate", api.workspaceAgentCoordinate)
|
||||
r.Get("/report-stats", api.workspaceAgentReportStats)
|
||||
r.Post("/report-stats", api.workspaceAgentReportStats)
|
||||
// DEPRECATED in favor of the POST endpoint above.
|
||||
// TODO: remove in January 2023
|
||||
r.Get("/report-stats", api.workspaceAgentReportStatsWebsocket)
|
||||
})
|
||||
r.Route("/{workspaceagent}", func(r chi.Router) {
|
||||
r.Use(
|
||||
|
@ -64,6 +64,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
|
||||
"POST:/api/v2/workspaceagents/me/version": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaceagents/me/app-health": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/me/report-stats": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaceagents/me/report-stats": {NoAuthorize: true},
|
||||
|
||||
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
||||
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(a.Admin.OrganizationID)},
|
||||
|
@ -30,31 +30,31 @@ func New() database.Store {
|
||||
return &fakeQuerier{
|
||||
mutex: &sync.RWMutex{},
|
||||
data: &data{
|
||||
apiKeys: make([]database.APIKey, 0),
|
||||
agentStats: make([]database.AgentStat, 0),
|
||||
organizationMembers: make([]database.OrganizationMember, 0),
|
||||
organizations: make([]database.Organization, 0),
|
||||
users: make([]database.User, 0),
|
||||
gitAuthLinks: make([]database.GitAuthLink, 0),
|
||||
groups: make([]database.Group, 0),
|
||||
groupMembers: make([]database.GroupMember, 0),
|
||||
auditLogs: make([]database.AuditLog, 0),
|
||||
files: make([]database.File, 0),
|
||||
gitSSHKey: make([]database.GitSSHKey, 0),
|
||||
parameterSchemas: make([]database.ParameterSchema, 0),
|
||||
parameterValues: make([]database.ParameterValue, 0),
|
||||
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
|
||||
provisionerJobAgents: make([]database.WorkspaceAgent, 0),
|
||||
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
|
||||
provisionerJobResources: make([]database.WorkspaceResource, 0),
|
||||
provisionerJobResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0),
|
||||
provisionerJobs: make([]database.ProvisionerJob, 0),
|
||||
templateVersions: make([]database.TemplateVersion, 0),
|
||||
templates: make([]database.Template, 0),
|
||||
workspaceBuilds: make([]database.WorkspaceBuild, 0),
|
||||
workspaceApps: make([]database.WorkspaceApp, 0),
|
||||
workspaces: make([]database.Workspace, 0),
|
||||
licenses: make([]database.License, 0),
|
||||
apiKeys: make([]database.APIKey, 0),
|
||||
agentStats: make([]database.AgentStat, 0),
|
||||
organizationMembers: make([]database.OrganizationMember, 0),
|
||||
organizations: make([]database.Organization, 0),
|
||||
users: make([]database.User, 0),
|
||||
gitAuthLinks: make([]database.GitAuthLink, 0),
|
||||
groups: make([]database.Group, 0),
|
||||
groupMembers: make([]database.GroupMember, 0),
|
||||
auditLogs: make([]database.AuditLog, 0),
|
||||
files: make([]database.File, 0),
|
||||
gitSSHKey: make([]database.GitSSHKey, 0),
|
||||
parameterSchemas: make([]database.ParameterSchema, 0),
|
||||
parameterValues: make([]database.ParameterValue, 0),
|
||||
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
|
||||
workspaceAgents: make([]database.WorkspaceAgent, 0),
|
||||
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
|
||||
workspaceResources: make([]database.WorkspaceResource, 0),
|
||||
workspaceResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0),
|
||||
provisionerJobs: make([]database.ProvisionerJob, 0),
|
||||
templateVersions: make([]database.TemplateVersion, 0),
|
||||
templates: make([]database.Template, 0),
|
||||
workspaceBuilds: make([]database.WorkspaceBuild, 0),
|
||||
workspaceApps: make([]database.WorkspaceApp, 0),
|
||||
workspaces: make([]database.Workspace, 0),
|
||||
licenses: make([]database.License, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -89,28 +89,28 @@ type data struct {
|
||||
userLinks []database.UserLink
|
||||
|
||||
// New tables
|
||||
agentStats []database.AgentStat
|
||||
auditLogs []database.AuditLog
|
||||
files []database.File
|
||||
gitAuthLinks []database.GitAuthLink
|
||||
gitSSHKey []database.GitSSHKey
|
||||
groups []database.Group
|
||||
groupMembers []database.GroupMember
|
||||
parameterSchemas []database.ParameterSchema
|
||||
parameterValues []database.ParameterValue
|
||||
provisionerDaemons []database.ProvisionerDaemon
|
||||
provisionerJobAgents []database.WorkspaceAgent
|
||||
provisionerJobLogs []database.ProvisionerJobLog
|
||||
provisionerJobResources []database.WorkspaceResource
|
||||
provisionerJobResourceMetadata []database.WorkspaceResourceMetadatum
|
||||
provisionerJobs []database.ProvisionerJob
|
||||
templateVersions []database.TemplateVersion
|
||||
templates []database.Template
|
||||
workspaceBuilds []database.WorkspaceBuild
|
||||
workspaceApps []database.WorkspaceApp
|
||||
workspaces []database.Workspace
|
||||
licenses []database.License
|
||||
replicas []database.Replica
|
||||
agentStats []database.AgentStat
|
||||
auditLogs []database.AuditLog
|
||||
files []database.File
|
||||
gitAuthLinks []database.GitAuthLink
|
||||
gitSSHKey []database.GitSSHKey
|
||||
groupMembers []database.GroupMember
|
||||
groups []database.Group
|
||||
licenses []database.License
|
||||
parameterSchemas []database.ParameterSchema
|
||||
parameterValues []database.ParameterValue
|
||||
provisionerDaemons []database.ProvisionerDaemon
|
||||
provisionerJobLogs []database.ProvisionerJobLog
|
||||
provisionerJobs []database.ProvisionerJob
|
||||
replicas []database.Replica
|
||||
templateVersions []database.TemplateVersion
|
||||
templates []database.Template
|
||||
workspaceAgents []database.WorkspaceAgent
|
||||
workspaceApps []database.WorkspaceApp
|
||||
workspaceBuilds []database.WorkspaceBuild
|
||||
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
|
||||
workspaceResources []database.WorkspaceResource
|
||||
workspaces []database.Workspace
|
||||
|
||||
deploymentID string
|
||||
derpMeshKey string
|
||||
@ -942,6 +942,52 @@ func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (databas
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceByAgentID(_ context.Context, agentID uuid.UUID) (database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
var agent database.WorkspaceAgent
|
||||
for _, _agent := range q.workspaceAgents {
|
||||
if _agent.ID == agentID {
|
||||
agent = _agent
|
||||
break
|
||||
}
|
||||
}
|
||||
if agent.ID == uuid.Nil {
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
var resource database.WorkspaceResource
|
||||
for _, _resource := range q.workspaceResources {
|
||||
if _resource.ID == agent.ResourceID {
|
||||
resource = _resource
|
||||
break
|
||||
}
|
||||
}
|
||||
if resource.ID == uuid.Nil {
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
var build database.WorkspaceBuild
|
||||
for _, _build := range q.workspaceBuilds {
|
||||
if _build.JobID == resource.JobID {
|
||||
build = _build
|
||||
break
|
||||
}
|
||||
}
|
||||
if build.ID == uuid.Nil {
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
for _, workspace := range q.workspaces {
|
||||
if workspace.ID == build.WorkspaceID {
|
||||
return workspace, nil
|
||||
}
|
||||
}
|
||||
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceByOwnerIDAndName(_ context.Context, arg database.GetWorkspaceByOwnerIDAndNameParams) (database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@ -1801,8 +1847,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
// The schema sorts this by created at, so we iterate the array backwards.
|
||||
for i := len(q.provisionerJobAgents) - 1; i >= 0; i-- {
|
||||
agent := q.provisionerJobAgents[i]
|
||||
for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
|
||||
agent := q.workspaceAgents[i]
|
||||
if agent.AuthToken == authToken {
|
||||
return agent, nil
|
||||
}
|
||||
@ -1815,8 +1861,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByID(_ context.Context, id uuid.UUID) (da
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
// The schema sorts this by created at, so we iterate the array backwards.
|
||||
for i := len(q.provisionerJobAgents) - 1; i >= 0; i-- {
|
||||
agent := q.provisionerJobAgents[i]
|
||||
for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
|
||||
agent := q.workspaceAgents[i]
|
||||
if agent.ID == id {
|
||||
return agent, nil
|
||||
}
|
||||
@ -1829,8 +1875,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
// The schema sorts this by created at, so we iterate the array backwards.
|
||||
for i := len(q.provisionerJobAgents) - 1; i >= 0; i-- {
|
||||
agent := q.provisionerJobAgents[i]
|
||||
for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
|
||||
agent := q.workspaceAgents[i]
|
||||
if agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID {
|
||||
return agent, nil
|
||||
}
|
||||
@ -1843,7 +1889,7 @@ func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourc
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceAgents := make([]database.WorkspaceAgent, 0)
|
||||
for _, agent := range q.provisionerJobAgents {
|
||||
for _, agent := range q.workspaceAgents {
|
||||
for _, resourceID := range resourceIDs {
|
||||
if agent.ResourceID != resourceID {
|
||||
continue
|
||||
@ -1859,7 +1905,7 @@ func (q *fakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after ti
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceAgents := make([]database.WorkspaceAgent, 0)
|
||||
for _, agent := range q.provisionerJobAgents {
|
||||
for _, agent := range q.workspaceAgents {
|
||||
if agent.CreatedAt.After(after) {
|
||||
workspaceAgents = append(workspaceAgents, agent)
|
||||
}
|
||||
@ -1913,7 +1959,7 @@ func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID)
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, resource := range q.provisionerJobResources {
|
||||
for _, resource := range q.workspaceResources {
|
||||
if resource.ID == id {
|
||||
return resource, nil
|
||||
}
|
||||
@ -1926,7 +1972,7 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
resources := make([]database.WorkspaceResource, 0)
|
||||
for _, resource := range q.provisionerJobResources {
|
||||
for _, resource := range q.workspaceResources {
|
||||
if resource.JobID != jobID {
|
||||
continue
|
||||
}
|
||||
@ -1940,7 +1986,7 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobIDs(_ context.Context, jobIDs []
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
resources := make([]database.WorkspaceResource, 0)
|
||||
for _, resource := range q.provisionerJobResources {
|
||||
for _, resource := range q.workspaceResources {
|
||||
for _, jobID := range jobIDs {
|
||||
if resource.JobID != jobID {
|
||||
continue
|
||||
@ -1956,7 +2002,7 @@ func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
resources := make([]database.WorkspaceResource, 0)
|
||||
for _, resource := range q.provisionerJobResources {
|
||||
for _, resource := range q.workspaceResources {
|
||||
if resource.CreatedAt.After(after) {
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
@ -1978,7 +2024,7 @@ func (q *fakeQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Conte
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
metadata := make([]database.WorkspaceResourceMetadatum, 0)
|
||||
for _, m := range q.provisionerJobResourceMetadata {
|
||||
for _, m := range q.workspaceResourceMetadata {
|
||||
_, ok := resourceIDs[m.WorkspaceResourceID]
|
||||
if !ok {
|
||||
continue
|
||||
@ -1993,7 +2039,7 @@ func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceID(_ context.Context
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
metadata := make([]database.WorkspaceResourceMetadatum, 0)
|
||||
for _, metadatum := range q.provisionerJobResourceMetadata {
|
||||
for _, metadatum := range q.workspaceResourceMetadata {
|
||||
if metadatum.WorkspaceResourceID == id {
|
||||
metadata = append(metadata, metadatum)
|
||||
}
|
||||
@ -2006,7 +2052,7 @@ func (q *fakeQuerier) GetWorkspaceResourceMetadataByResourceIDs(_ context.Contex
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
metadata := make([]database.WorkspaceResourceMetadatum, 0)
|
||||
for _, metadatum := range q.provisionerJobResourceMetadata {
|
||||
for _, metadatum := range q.workspaceResourceMetadata {
|
||||
for _, id := range ids {
|
||||
if metadatum.WorkspaceResourceID == id {
|
||||
metadata = append(metadata, metadatum)
|
||||
@ -2319,7 +2365,7 @@ func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser
|
||||
TroubleshootingURL: arg.TroubleshootingURL,
|
||||
}
|
||||
|
||||
q.provisionerJobAgents = append(q.provisionerJobAgents, agent)
|
||||
q.workspaceAgents = append(q.workspaceAgents, agent)
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
@ -2339,7 +2385,7 @@ func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In
|
||||
Icon: arg.Icon,
|
||||
DailyCost: arg.DailyCost,
|
||||
}
|
||||
q.provisionerJobResources = append(q.provisionerJobResources, resource)
|
||||
q.workspaceResources = append(q.workspaceResources, resource)
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
@ -2354,7 +2400,7 @@ func (q *fakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat
|
||||
Value: arg.Value,
|
||||
Sensitive: arg.Sensitive,
|
||||
}
|
||||
q.provisionerJobResourceMetadata = append(q.provisionerJobResourceMetadata, metadatum)
|
||||
q.workspaceResourceMetadata = append(q.workspaceResourceMetadata, metadatum)
|
||||
return metadatum, nil
|
||||
}
|
||||
|
||||
@ -2681,7 +2727,7 @@ func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, agent := range q.provisionerJobAgents {
|
||||
for index, agent := range q.workspaceAgents {
|
||||
if agent.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
@ -2689,7 +2735,7 @@ func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg
|
||||
agent.LastConnectedAt = arg.LastConnectedAt
|
||||
agent.DisconnectedAt = arg.DisconnectedAt
|
||||
agent.UpdatedAt = arg.UpdatedAt
|
||||
q.provisionerJobAgents[index] = agent
|
||||
q.workspaceAgents[index] = agent
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
@ -2699,13 +2745,13 @@ func (q *fakeQuerier) UpdateWorkspaceAgentVersionByID(_ context.Context, arg dat
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, agent := range q.provisionerJobAgents {
|
||||
for index, agent := range q.workspaceAgents {
|
||||
if agent.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
agent.Version = arg.Version
|
||||
q.provisionerJobAgents[index] = agent
|
||||
q.workspaceAgents[index] = agent
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
|
@ -113,6 +113,7 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
|
||||
GetWorkspaceCountByUserID(ctx context.Context, ownerID uuid.UUID) (int64, error)
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
const deleteOldAgentStats = `-- name: DeleteOldAgentStats :exec
|
||||
DELETE FROM AGENT_STATS WHERE created_at < now() - interval '30 days'
|
||||
DELETE FROM agent_stats WHERE created_at < NOW() - INTERVAL '30 days'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteOldAgentStats(ctx context.Context) error {
|
||||
@ -45,16 +45,17 @@ func (q *sqlQuerier) GetLatestAgentStat(ctx context.Context, agentID uuid.UUID)
|
||||
}
|
||||
|
||||
const getTemplateDAUs = `-- name: GetTemplateDAUs :many
|
||||
select
|
||||
SELECT
|
||||
(created_at at TIME ZONE 'UTC')::date as date,
|
||||
user_id
|
||||
from
|
||||
FROM
|
||||
agent_stats
|
||||
where template_id = $1
|
||||
group by
|
||||
WHERE
|
||||
template_id = $1
|
||||
GROUP BY
|
||||
date, user_id
|
||||
order by
|
||||
date asc
|
||||
ORDER BY
|
||||
date ASC
|
||||
`
|
||||
|
||||
type GetTemplateDAUsRow struct {
|
||||
@ -6145,6 +6146,55 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
workspaces.id = (
|
||||
SELECT
|
||||
workspace_id
|
||||
FROM
|
||||
workspace_builds
|
||||
WHERE
|
||||
workspace_builds.job_id = (
|
||||
SELECT
|
||||
job_id
|
||||
FROM
|
||||
workspace_resources
|
||||
WHERE
|
||||
workspace_resources.id = (
|
||||
SELECT
|
||||
resource_id
|
||||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
workspace_agents.id = $1
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceByAgentID, agentID)
|
||||
var i Workspace
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||
|
@ -16,16 +16,17 @@ VALUES
|
||||
SELECT * FROM agent_stats WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1;
|
||||
|
||||
-- name: GetTemplateDAUs :many
|
||||
select
|
||||
SELECT
|
||||
(created_at at TIME ZONE 'UTC')::date as date,
|
||||
user_id
|
||||
from
|
||||
FROM
|
||||
agent_stats
|
||||
where template_id = $1
|
||||
group by
|
||||
WHERE
|
||||
template_id = $1
|
||||
GROUP BY
|
||||
date, user_id
|
||||
order by
|
||||
date asc;
|
||||
ORDER BY
|
||||
date ASC;
|
||||
|
||||
-- name: DeleteOldAgentStats :exec
|
||||
DELETE FROM AGENT_STATS WHERE created_at < now() - interval '30 days';
|
||||
DELETE FROM agent_stats WHERE created_at < NOW() - INTERVAL '30 days';
|
||||
|
@ -8,6 +8,35 @@ WHERE
|
||||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspaceByAgentID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
workspaces.id = (
|
||||
SELECT
|
||||
workspace_id
|
||||
FROM
|
||||
workspace_builds
|
||||
WHERE
|
||||
workspace_builds.job_id = (
|
||||
SELECT
|
||||
job_id
|
||||
FROM
|
||||
workspace_resources
|
||||
WHERE
|
||||
workspace_resources.id = (
|
||||
SELECT
|
||||
resource_id
|
||||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
workspace_agents.id = @agent_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- name: GetWorkspaces :many
|
||||
SELECT
|
||||
workspaces.*, COUNT(*) OVER () as count
|
||||
|
@ -75,41 +75,41 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
return
|
||||
}
|
||||
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID)
|
||||
dbApps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace agent applications.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), workspaceAgent.ResourceID)
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace resource.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
build, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID)
|
||||
build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), build.WorkspaceID)
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
owner, err := api.Database.GetUserByID(r.Context(), workspace.OwnerID)
|
||||
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace owner.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
@ -755,31 +755,69 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
|
||||
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.AgentStats
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.RxBytes == 0 && req.TxBytes == 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentStatsResponse{
|
||||
ReportInterval: api.AgentStatsRefreshInterval,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
activityBumpWorkspace(api.Logger.Named("activity_bump"), api.Database, workspace.ID)
|
||||
|
||||
now := database.Now()
|
||||
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
AgentID: workspaceAgent.ID,
|
||||
WorkspaceID: workspace.ID,
|
||||
UserID: workspace.OwnerID,
|
||||
TemplateID: workspace.TemplateID,
|
||||
Payload: json.RawMessage("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{
|
||||
ID: workspace.ID,
|
||||
LastUsedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AgentStatsResponse{
|
||||
ReportInterval: api.AgentStatsRefreshInterval,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) workspaceAgentReportStatsWebsocket(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
api.WebsocketWaitMutex.Lock()
|
||||
api.WebsocketWaitGroup.Add(1)
|
||||
api.WebsocketWaitMutex.Unlock()
|
||||
defer api.WebsocketWaitGroup.Done()
|
||||
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get workspace resource.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to get workspace.",
|
||||
@ -861,14 +899,13 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||
api.Logger.Debug(ctx, "read stats report",
|
||||
slog.F("interval", api.AgentStatsRefreshInterval),
|
||||
slog.F("agent", workspaceAgent.ID),
|
||||
slog.F("resource", resource.ID),
|
||||
slog.F("workspace", workspace.ID),
|
||||
slog.F("update_db", updateDB),
|
||||
slog.F("payload", rep),
|
||||
)
|
||||
|
||||
if updateDB {
|
||||
go activityBumpWorkspace(api.Logger.Named("activity_bump"), api.Database, workspace)
|
||||
go activityBumpWorkspace(api.Logger.Named("activity_bump"), api.Database, workspace.ID)
|
||||
|
||||
lastReport = rep
|
||||
|
||||
@ -876,7 +913,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
AgentID: workspaceAgent.ID,
|
||||
WorkspaceID: build.WorkspaceID,
|
||||
WorkspaceID: workspace.ID,
|
||||
UserID: workspace.OwnerID,
|
||||
TemplateID: workspace.TemplateID,
|
||||
Payload: json.RawMessage(repJSON),
|
||||
@ -888,7 +925,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
err = api.Database.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{
|
||||
ID: build.WorkspaceID,
|
||||
ID: workspace.ID,
|
||||
LastUsedAt: database.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
@ -901,22 +938,23 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
var req codersdk.PostWorkspaceAppHealthsRequest
|
||||
if !httpapi.Read(r.Context(), rw, r, &req) {
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Healths == nil || len(req.Healths) == 0 {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Health field is empty",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apps, err := api.Database.GetWorkspaceAppsByAgentID(r.Context(), workspaceAgent.ID)
|
||||
apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error getting agent apps",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
@ -935,7 +973,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
return nil
|
||||
}()
|
||||
if old == nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Error setting workspace app health",
|
||||
Detail: xerrors.Errorf("workspace app name %s not found", id).Error(),
|
||||
})
|
||||
@ -943,7 +981,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if old.HealthcheckUrl == "" {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Error setting workspace app health",
|
||||
Detail: xerrors.Errorf("health checking is disabled for workspace app %s", id).Error(),
|
||||
})
|
||||
@ -955,7 +993,7 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
case codersdk.WorkspaceAppHealthHealthy:
|
||||
case codersdk.WorkspaceAppHealthUnhealthy:
|
||||
default:
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Error setting workspace app health",
|
||||
Detail: xerrors.Errorf("workspace app health %s is not a valid value", newHealth).Error(),
|
||||
})
|
||||
@ -972,12 +1010,12 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
for _, app := range newApps {
|
||||
err = api.Database.UpdateWorkspaceAppHealthByID(r.Context(), database.UpdateWorkspaceAppHealthByIDParams{
|
||||
err = api.Database.UpdateWorkspaceAppHealthByID(ctx, database.UpdateWorkspaceAppHealthByIDParams{
|
||||
ID: app.ID,
|
||||
Health: app.Health,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error setting workspace app health",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
@ -985,33 +1023,33 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), workspaceAgent.ResourceID)
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace resource.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID)
|
||||
job, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), job.WorkspaceID)
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, job.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
api.publishWorkspaceUpdate(r.Context(), workspace.ID)
|
||||
api.publishWorkspaceUpdate(ctx, workspace.ID)
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, nil)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// postWorkspaceAgentsGitAuth returns a username and password for use
|
||||
@ -1101,7 +1139,7 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
case <-authChan:
|
||||
|
@ -1065,6 +1065,65 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceAgentReportStats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
ProvisionApply: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "aws_instance",
|
||||
Agents: []*proto.Agent{{
|
||||
Id: uuid.NewString(),
|
||||
Auth: &proto.Agent_Token{
|
||||
Token: authToken,
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
agentClient := codersdk.New(client.URL)
|
||||
agentClient.SetSessionToken(authToken)
|
||||
|
||||
_, err := agentClient.PostAgentStats(context.Background(), &codersdk.AgentStats{
|
||||
ConnsByProto: map[string]int64{"TCP": 1},
|
||||
NumConns: 1,
|
||||
RxPackets: 1,
|
||||
RxBytes: 1,
|
||||
TxPackets: 1,
|
||||
TxBytes: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
newWorkspace, err := client.Workspace(context.Background(), workspace.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t,
|
||||
newWorkspace.LastUsedAt.After(workspace.LastUsedAt),
|
||||
"%s is not after %s", newWorkspace.LastUsedAt, workspace.LastUsedAt,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func gitAuthCallback(t *testing.T, id string, client *codersdk.Client) *http.Response {
|
||||
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
|
Reference in New Issue
Block a user