diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c291464604..519684eb7c 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3552,6 +3552,13 @@ func (q *querier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt return q.db.GetWorkspaceAgentsCreatedAfter(ctx, createdAt) } +func (q *querier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]database.GetWorkspaceAgentsForMetricsRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace); err != nil { + return nil, err + } + return q.db.GetWorkspaceAgentsForMetrics(ctx) +} + func (q *querier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { workspace, err := q.GetWorkspaceByID(ctx, workspaceID) if err != nil { @@ -3857,6 +3864,13 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti return q.db.GetWorkspacesEligibleForTransition(ctx, now) } +func (q *querier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]database.GetWorkspacesForWorkspaceMetricsRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace); err != nil { + return nil, err + } + return q.db.GetWorkspacesForWorkspaceMetrics(ctx) +} + func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) { return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f65a65c322..989f9c866b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1689,6 +1689,15 @@ func (s *MethodTestSuite) TestWorkspace() { // No asserts here because SQLFilter. check.Args(arg).Asserts() })) + s.Run("GetWorkspaceAgentsForMetrics", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + row := testutil.Fake(s.T(), faker, database.GetWorkspaceAgentsForMetricsRow{}) + dbm.EXPECT().GetWorkspaceAgentsForMetrics(gomock.Any()).Return([]database.GetWorkspaceAgentsForMetricsRow{row}, nil).AnyTimes() + check.Args().Asserts(rbac.ResourceWorkspace, policy.ActionRead).Returns([]database.GetWorkspaceAgentsForMetricsRow{row}) + })) + s.Run("GetWorkspacesForWorkspaceMetrics", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetWorkspacesForWorkspaceMetrics(gomock.Any()).Return([]database.GetWorkspacesForWorkspaceMetricsRow{}, nil).AnyTimes() + check.Args().Asserts(rbac.ResourceWorkspace, policy.ActionRead) + })) s.Run("GetAuthorizedWorkspaces", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { arg := database.GetWorkspacesParams{} dbm.EXPECT().GetAuthorizedWorkspaces(gomock.Any(), arg, gomock.Any()).Return([]database.GetWorkspacesRow{}, nil).AnyTimes() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 675cc81b01..d5995222c3 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1972,6 +1972,13 @@ func (m queryMetricsStore) GetWorkspaceAgentsCreatedAfter(ctx context.Context, c return agents, err } +func (m queryMetricsStore) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]database.GetWorkspaceAgentsForMetricsRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAgentsForMetrics(ctx) + m.queryLatencies.WithLabelValues("GetWorkspaceAgentsForMetrics").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { start := time.Now() agents, err := m.s.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspaceID) @@ -2224,6 +2231,13 @@ func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Contex return workspaces, err } +func (m queryMetricsStore) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]database.GetWorkspacesForWorkspaceMetricsRow, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspacesForWorkspaceMetrics(ctx) + m.queryLatencies.WithLabelValues("GetWorkspacesForWorkspaceMetrics").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) { start := time.Now() r0, r1 := m.s.InsertAIBridgeInterception(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index d6e07b77da..51a06fa35e 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4199,6 +4199,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentsCreatedAfter(ctx, createdAt a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsCreatedAfter), ctx, createdAt) } +// GetWorkspaceAgentsForMetrics mocks base method. +func (m *MockStore) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]database.GetWorkspaceAgentsForMetricsRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAgentsForMetrics", ctx) + ret0, _ := ret[0].([]database.GetWorkspaceAgentsForMetricsRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAgentsForMetrics indicates an expected call of GetWorkspaceAgentsForMetrics. +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsForMetrics(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsForMetrics", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsForMetrics), ctx) +} + // GetWorkspaceAgentsInLatestBuildByWorkspaceID mocks base method. func (m *MockStore) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgent, error) { m.ctrl.T.Helper() @@ -4739,6 +4754,21 @@ func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(ctx, now any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), ctx, now) } +// GetWorkspacesForWorkspaceMetrics mocks base method. +func (m *MockStore) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]database.GetWorkspacesForWorkspaceMetricsRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspacesForWorkspaceMetrics", ctx) + ret0, _ := ret[0].([]database.GetWorkspacesForWorkspaceMetricsRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspacesForWorkspaceMetrics indicates an expected call of GetWorkspacesForWorkspaceMetrics. +func (mr *MockStoreMockRecorder) GetWorkspacesForWorkspaceMetrics(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesForWorkspaceMetrics", reflect.TypeOf((*MockStore)(nil).GetWorkspacesForWorkspaceMetrics), ctx) +} + // InTx mocks base method. func (m *MockStore) InTx(arg0 func(database.Store) error, arg1 *database.TxOptions) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 097d8901dd..3ea3c912f8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -464,6 +464,7 @@ type sqlcQuerier interface { GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) + GetWorkspaceAgentsForMetrics(ctx context.Context) ([]GetWorkspaceAgentsForMetricsRow, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) @@ -510,6 +511,7 @@ type sqlcQuerier interface { GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) + GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9319416c56..f62066954d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -17645,6 +17645,102 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created return items, nil } +const getWorkspaceAgentsForMetrics = `-- name: GetWorkspaceAgentsForMetrics :many +SELECT + w.id as workspace_id, + w.name as workspace_name, + u.username as owner_username, + t.name as template_name, + tv.name as template_version_name, + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted +FROM workspaces w +JOIN users u ON w.owner_id = u.id +JOIN templates t ON w.template_id = t.id +JOIN workspace_builds wb ON w.id = wb.workspace_id +LEFT JOIN template_versions tv ON wb.template_version_id = tv.id +JOIN workspace_resources wr ON wb.job_id = wr.job_id +JOIN workspace_agents ON wr.id = workspace_agents.resource_id +WHERE w.deleted = false +AND wb.build_number = ( + SELECT MAX(wb2.build_number) + FROM workspace_builds wb2 + WHERE wb2.workspace_id = w.id +) +AND workspace_agents.deleted = FALSE +` + +type GetWorkspaceAgentsForMetricsRow struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + WorkspaceName string `db:"workspace_name" json:"workspace_name"` + OwnerUsername string `db:"owner_username" json:"owner_username"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` + WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"` +} + +func (q *sqlQuerier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]GetWorkspaceAgentsForMetricsRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsForMetrics) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspaceAgentsForMetricsRow + for rows.Next() { + var i GetWorkspaceAgentsForMetricsRow + if err := rows.Scan( + &i.WorkspaceID, + &i.WorkspaceName, + &i.OwnerUsername, + &i.TemplateName, + &i.TemplateVersionName, + &i.WorkspaceAgent.ID, + &i.WorkspaceAgent.CreatedAt, + &i.WorkspaceAgent.UpdatedAt, + &i.WorkspaceAgent.Name, + &i.WorkspaceAgent.FirstConnectedAt, + &i.WorkspaceAgent.LastConnectedAt, + &i.WorkspaceAgent.DisconnectedAt, + &i.WorkspaceAgent.ResourceID, + &i.WorkspaceAgent.AuthToken, + &i.WorkspaceAgent.AuthInstanceID, + &i.WorkspaceAgent.Architecture, + &i.WorkspaceAgent.EnvironmentVariables, + &i.WorkspaceAgent.OperatingSystem, + &i.WorkspaceAgent.InstanceMetadata, + &i.WorkspaceAgent.ResourceMetadata, + &i.WorkspaceAgent.Directory, + &i.WorkspaceAgent.Version, + &i.WorkspaceAgent.LastConnectedReplicaID, + &i.WorkspaceAgent.ConnectionTimeoutSeconds, + &i.WorkspaceAgent.TroubleshootingURL, + &i.WorkspaceAgent.MOTDFile, + &i.WorkspaceAgent.LifecycleState, + &i.WorkspaceAgent.ExpandedDirectory, + &i.WorkspaceAgent.LogsLength, + &i.WorkspaceAgent.LogsOverflowed, + &i.WorkspaceAgent.StartedAt, + &i.WorkspaceAgent.ReadyAt, + pq.Array(&i.WorkspaceAgent.Subsystems), + pq.Array(&i.WorkspaceAgent.DisplayApps), + &i.WorkspaceAgent.APIVersion, + &i.WorkspaceAgent.DisplayOrder, + &i.WorkspaceAgent.ParentID, + &i.WorkspaceAgent.APIKeyScope, + &i.WorkspaceAgent.Deleted, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many SELECT workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted @@ -22354,6 +22450,64 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now return items, nil } +const getWorkspacesForWorkspaceMetrics = `-- name: GetWorkspacesForWorkspaceMetrics :many +SELECT + u.username as owner_username, + t.name as template_name, + tv.name as template_version_name, + pj.job_status as latest_build_status, + wb.transition as latest_build_transition +FROM workspaces w +JOIN users u ON w.owner_id = u.id +JOIN templates t ON w.template_id = t.id +JOIN workspace_builds wb ON w.id = wb.workspace_id +JOIN provisioner_jobs pj ON wb.job_id = pj.id +LEFT JOIN template_versions tv ON wb.template_version_id = tv.id +WHERE w.deleted = false +AND wb.build_number = ( + SELECT MAX(wb2.build_number) + FROM workspace_builds wb2 + WHERE wb2.workspace_id = w.id +) +` + +type GetWorkspacesForWorkspaceMetricsRow struct { + OwnerUsername string `db:"owner_username" json:"owner_username"` + TemplateName string `db:"template_name" json:"template_name"` + TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` + LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"` + LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` +} + +func (q *sqlQuerier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error) { + rows, err := q.db.QueryContext(ctx, getWorkspacesForWorkspaceMetrics) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWorkspacesForWorkspaceMetricsRow + for rows.Next() { + var i GetWorkspacesForWorkspaceMetricsRow + if err := rows.Scan( + &i.OwnerUsername, + &i.TemplateName, + &i.TemplateVersionName, + &i.LatestBuildStatus, + &i.LatestBuildTransition, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const insertWorkspace = `-- name: InsertWorkspace :one INSERT INTO workspaces ( diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index c67435d7cb..cc59e96544 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -365,3 +365,26 @@ WHERE id = $1 AND parent_id IS NOT NULL AND deleted = FALSE; + +-- name: GetWorkspaceAgentsForMetrics :many +SELECT + w.id as workspace_id, + w.name as workspace_name, + u.username as owner_username, + t.name as template_name, + tv.name as template_version_name, + sqlc.embed(workspace_agents) +FROM workspaces w +JOIN users u ON w.owner_id = u.id +JOIN templates t ON w.template_id = t.id +JOIN workspace_builds wb ON w.id = wb.workspace_id +LEFT JOIN template_versions tv ON wb.template_version_id = tv.id +JOIN workspace_resources wr ON wb.job_id = wr.job_id +JOIN workspace_agents ON wr.id = workspace_agents.resource_id +WHERE w.deleted = false +AND wb.build_number = ( + SELECT MAX(wb2.build_number) + FROM workspace_builds wb2 + WHERE wb2.workspace_id = w.id +) +AND workspace_agents.deleted = FALSE; diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 7ab67e98e4..8ccc69b9a8 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -983,3 +983,23 @@ WHERE AND fsb.initiator_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid GROUP BY t.name, COALESCE(tvp.name, ''), o.name ORDER BY t.name, preset_name, o.name; + +-- name: GetWorkspacesForWorkspaceMetrics :many +SELECT + u.username as owner_username, + t.name as template_name, + tv.name as template_version_name, + pj.job_status as latest_build_status, + wb.transition as latest_build_transition +FROM workspaces w +JOIN users u ON w.owner_id = u.id +JOIN templates t ON w.template_id = t.id +JOIN workspace_builds wb ON w.id = wb.workspace_id +JOIN provisioner_jobs pj ON wb.job_id = pj.id +LEFT JOIN template_versions tv ON wb.template_version_id = tv.id +WHERE w.deleted = false +AND wb.build_number = ( + SELECT MAX(wb2.build_number) + FROM workspace_builds wb2 + WHERE wb2.workspace_id = w.id +); diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go index ed55e4598d..525ec66c5a 100644 --- a/coderd/prometheusmetrics/prometheusmetrics.go +++ b/coderd/prometheusmetrics/prometheusmetrics.go @@ -181,10 +181,8 @@ func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.R done := make(chan struct{}) updateWorkspaceMetrics := func() { - ws, err := db.GetWorkspaces(ctx, database.GetWorkspacesParams{ - Deleted: false, - WithSummary: false, - }) + // Don't count deleted workspaces as part of these metrics. + ws, err := db.GetWorkspacesForWorkspaceMetrics(ctx) if err != nil { if errors.Is(err, sql.ErrNoRows) { workspaceLatestBuildTotals.Reset() @@ -346,88 +344,67 @@ func Agents(ctx context.Context, logger slog.Logger, registerer prometheus.Regis timer := prometheus.NewTimer(metricsCollectorAgents) derpMap := derpMapFn() - workspaceRows, err := db.GetWorkspaces(ctx, database.GetWorkspacesParams{ - AgentInactiveDisconnectTimeoutSeconds: int64(agentInactiveDisconnectTimeout.Seconds()), - }) + workspaceAgents, err := db.GetWorkspaceAgentsForMetrics(ctx) if err != nil { - logger.Error(ctx, "can't get workspace rows", slog.Error(err)) + logger.Error(ctx, "can't get workspace agents", slog.Error(err)) goto done } - for _, workspace := range workspaceRows { - templateName := workspace.TemplateName - templateVersionName := workspace.TemplateVersionName.String - if !workspace.TemplateVersionName.Valid { + for _, agent := range workspaceAgents { + // Collect information about agents + templateVersionName := agent.TemplateVersionName.String + if !agent.TemplateVersionName.Valid { templateVersionName = "unknown" } + agentsGauge.WithLabelValues(VectorOperationAdd, 1, agent.OwnerUsername, agent.WorkspaceName, agent.TemplateName, templateVersionName) - // username := + connectionStatus := agent.WorkspaceAgent.Status(agentInactiveDisconnectTimeout) + node := (*coordinator.Load()).Node(agent.WorkspaceAgent.ID) - agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) - if err != nil { - logger.Error(ctx, "can't get workspace agents", slog.F("workspace_id", workspace.ID), slog.Error(err)) - agentsGauge.WithLabelValues(VectorOperationAdd, 0, workspace.OwnerUsername, workspace.Name, templateName, templateVersionName) - continue + tailnetNode := "unknown" + if node != nil { + tailnetNode = node.ID.String() } - if len(agents) == 0 { - logger.Debug(ctx, "workspace agents are unavailable", slog.F("workspace_id", workspace.ID)) - agentsGauge.WithLabelValues(VectorOperationAdd, 0, workspace.OwnerUsername, workspace.Name, templateName, templateVersionName) - continue - } + agentsConnectionsGauge.WithLabelValues(VectorOperationSet, 1, agent.WorkspaceAgent.Name, agent.OwnerUsername, agent.WorkspaceName, string(connectionStatus.Status), string(agent.WorkspaceAgent.LifecycleState), tailnetNode) - for _, agent := range agents { - // Collect information about agents - agentsGauge.WithLabelValues(VectorOperationAdd, 1, workspace.OwnerUsername, workspace.Name, templateName, templateVersionName) - - connectionStatus := agent.Status(agentInactiveDisconnectTimeout) - node := (*coordinator.Load()).Node(agent.ID) - - tailnetNode := "unknown" - if node != nil { - tailnetNode = node.ID.String() - } - - agentsConnectionsGauge.WithLabelValues(VectorOperationSet, 1, agent.Name, workspace.OwnerUsername, workspace.Name, string(connectionStatus.Status), string(agent.LifecycleState), tailnetNode) - - if node == nil { - logger.Debug(ctx, "can't read in-memory node for agent", slog.F("agent_id", agent.ID)) - } else { - // Collect information about connection latencies - for rawRegion, latency := range node.DERPLatency { - regionParts := strings.SplitN(rawRegion, "-", 2) - regionID, err := strconv.Atoi(regionParts[0]) - if err != nil { - logger.Error(ctx, "can't convert DERP region", slog.F("agent_id", agent.ID), slog.F("raw_region", rawRegion), slog.Error(err)) - continue - } - - region, found := derpMap.Regions[regionID] - if !found { - // It's possible that a workspace agent is using an old DERPMap - // and reports regions that do not exist. If that's the case, - // report the region as unknown! - region = &tailcfg.DERPRegion{ - RegionID: regionID, - RegionName: fmt.Sprintf("Unnamed %d", regionID), - } - } - - agentsConnectionLatenciesGauge.WithLabelValues(VectorOperationSet, latency, agent.Name, workspace.OwnerUsername, workspace.Name, region.RegionName, fmt.Sprintf("%v", node.PreferredDERP == regionID)) + if node == nil { + logger.Debug(ctx, "can't read in-memory node for agent", slog.F("agent_id", agent.WorkspaceAgent.ID)) + } else { + // Collect information about connection latencies + for rawRegion, latency := range node.DERPLatency { + regionParts := strings.SplitN(rawRegion, "-", 2) + regionID, err := strconv.Atoi(regionParts[0]) + if err != nil { + logger.Error(ctx, "can't convert DERP region", slog.F("agent_id", agent.WorkspaceAgent.ID), slog.F("raw_region", rawRegion), slog.Error(err)) + continue } - } - // Collect information about registered applications - apps, err := db.GetWorkspaceAppsByAgentID(ctx, agent.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - logger.Error(ctx, "can't get workspace apps", slog.F("agent_id", agent.ID), slog.Error(err)) - continue - } + region, found := derpMap.Regions[regionID] + if !found { + // It's possible that a workspace agent is using an old DERPMap + // and reports regions that do not exist. If that's the case, + // report the region as unknown! + region = &tailcfg.DERPRegion{ + RegionID: regionID, + RegionName: fmt.Sprintf("Unnamed %d", regionID), + } + } - for _, app := range apps { - agentsAppsGauge.WithLabelValues(VectorOperationAdd, 1, agent.Name, workspace.OwnerUsername, workspace.Name, app.DisplayName, string(app.Health)) + agentsConnectionLatenciesGauge.WithLabelValues(VectorOperationSet, latency, agent.WorkspaceAgent.Name, agent.OwnerUsername, agent.WorkspaceName, region.RegionName, fmt.Sprintf("%v", node.PreferredDERP == regionID)) } } + + // Collect information about registered applications + apps, err := db.GetWorkspaceAppsByAgentID(ctx, agent.WorkspaceAgent.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + logger.Error(ctx, "can't get workspace apps", slog.F("agent_id", agent.WorkspaceAgent.ID), slog.Error(err)) + continue + } + + for _, app := range apps { + agentsAppsGauge.WithLabelValues(VectorOperationAdd, 1, agent.WorkspaceAgent.Name, agent.OwnerUsername, agent.WorkspaceName, app.DisplayName, string(app.Health)) + } } agentsGauge.Commit() diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go index e75f86e51b..0d115e0840 100644 --- a/coderd/prometheusmetrics/prometheusmetrics_test.go +++ b/coderd/prometheusmetrics/prometheusmetrics_test.go @@ -291,6 +291,7 @@ func TestWorkspaceLatestBuildTotals(t *testing.T) { } for _, metric := range m.Metric { + fmt.Printf("metric: %+v\n", metric) count, ok := tc.Status[codersdk.ProvisionerJobStatus(metric.Label[0].GetValue())] if metric.Gauge.GetValue() == 0 { continue