From 8f1f1417be60c26d2797f092d31f7a22bdc512d6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Mar 2023 10:53:22 -0600 Subject: [PATCH 01/24] fix: don't make session counts cumulative This made for some weird tracking... we want the point-in-time number of counts! --- agent/agent.go | 7 +++- agent/agent_test.go | 96 ++++++++++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 6b9adfcac83c0..0d23bfb4ac8b1 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -879,10 +879,13 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) { switch magicType { case MagicSSHSessionTypeVSCode: a.connCountVSCode.Add(1) + defer a.connCountVSCode.Add(-1) case MagicSSHSessionTypeJetBrains: a.connCountJetBrains.Add(1) + defer a.connCountJetBrains.Add(-1) case "": a.connCountSSHSession.Add(1) + defer a.connCountSSHSession.Add(-1) default: a.logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType)) } @@ -986,6 +989,7 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m defer conn.Close() a.connCountReconnectingPTY.Add(1) + defer a.connCountReconnectingPTY.Add(-1) connectionID := uuid.NewString() logger = logger.With(slog.F("id", msg.ID), slog.F("connection_id", connectionID)) @@ -1194,8 +1198,7 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { stats.TxPackets = a.statTxPackets.Add(int64(counts.TxPackets)) } - // Tailscale's connection stats are not cumulative, but it makes no sense to make - // ours temporary. + // The count of active sessions. stats.SessionCountSSH = a.connCountSSHSession.Load() stats.SessionCountVSCode = a.connCountVSCode.Load() stats.SessionCountJetBrains = a.connCountJetBrains.Load() diff --git a/agent/agent_test.go b/agent/agent_test.go index 0a72bbeaa10a1..e5ceb22182c97 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -110,42 +110,68 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) { func TestAgent_Stats_Magic(t *testing.T) { t.Parallel() + t.Run("StripsEnvironmentVariable", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + sshClient, err := conn.SSHClient(ctx) + require.NoError(t, err) + defer sshClient.Close() + session, err := sshClient.NewSession() + require.NoError(t, err) + session.Setenv(agent.MagicSSHSessionTypeEnvironmentVariable, agent.MagicSSHSessionTypeVSCode) + defer session.Close() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) - sshClient, err := conn.SSHClient(ctx) - require.NoError(t, err) - defer sshClient.Close() - session, err := sshClient.NewSession() - require.NoError(t, err) - session.Setenv(agent.MagicSSHSessionTypeEnvironmentVariable, agent.MagicSSHSessionTypeVSCode) - defer session.Close() - - command := "sh -c 'echo $" + agent.MagicSSHSessionTypeEnvironmentVariable + "'" - expected := "" - if runtime.GOOS == "windows" { - expected = "%" + agent.MagicSSHSessionTypeEnvironmentVariable + "%" - command = "cmd.exe /c echo " + expected - } - output, err := session.Output(command) - require.NoError(t, err) - require.Equal(t, expected, strings.TrimSpace(string(output))) - var s *agentsdk.Stats - require.Eventuallyf(t, func() bool { - var ok bool - s, ok = <-stats - return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && - // Ensure that the connection didn't count as a "normal" SSH session. - // This was a special one, so it should be labeled specially in the stats! - s.SessionCountVSCode == 1 && - // Ensure that connection latency is being counted! - // If it isn't, it's set to -1. - s.ConnectionMedianLatencyMS >= 0 - }, testutil.WaitLong, testutil.IntervalFast, - "never saw stats: %+v", s, - ) + command := "sh -c 'echo $" + agent.MagicSSHSessionTypeEnvironmentVariable + "'" + expected := "" + if runtime.GOOS == "windows" { + expected = "%" + agent.MagicSSHSessionTypeEnvironmentVariable + "%" + command = "cmd.exe /c echo " + expected + } + output, err := session.Output(command) + require.NoError(t, err) + require.Equal(t, expected, strings.TrimSpace(string(output))) + }) + t.Run("Tracks", func(t *testing.T) { + t.Parallel() + if runtime.GOOS == "window" { + t.Skip("Sleeping for infinity doesn't work on Windows") + } + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + conn, _, stats, _ := setupAgent(t, agentsdk.Metadata{}, 0) + sshClient, err := conn.SSHClient(ctx) + require.NoError(t, err) + defer sshClient.Close() + session, err := sshClient.NewSession() + require.NoError(t, err) + session.Setenv(agent.MagicSSHSessionTypeEnvironmentVariable, agent.MagicSSHSessionTypeVSCode) + defer session.Close() + stdin, err := session.StdinPipe() + require.NoError(t, err) + err = session.Shell() + require.NoError(t, err) + var s *agentsdk.Stats + require.Eventuallyf(t, func() bool { + var ok bool + s, ok = <-stats + fmt.Printf("WE GOT STATS %+v\n", s) + return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && + // Ensure that the connection didn't count as a "normal" SSH session. + // This was a special one, so it should be labeled specially in the stats! + s.SessionCountVSCode == 1 && + // Ensure that connection latency is being counted! + // If it isn't, it's set to -1. + s.ConnectionMedianLatencyMS >= 0 + }, testutil.WaitLong, testutil.IntervalFast, + "never saw stats: %+v", s, + ) + // The shell will automatically exit if there is no stdin! + _ = stdin.Close() + err = session.Wait() + require.NoError(t, err) + }) } func TestAgent_SessionExec(t *testing.T) { From ddf9841c2ccc303c47ac67504c36211ebb96d542 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Mar 2023 11:55:52 -0600 Subject: [PATCH 02/24] Add databasefake query for getting agent stats --- agent/agent.go | 16 ++----- coderd/database/dbfake/databasefake.go | 45 ++++++++++++++++++ coderd/database/generate.sh | 2 +- coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 46 +++++++++++++++++++ .../database/queries/workspaceagentstats.sql | 18 ++++++++ codersdk/deployment.go | 21 +++++++++ 7 files changed, 137 insertions(+), 12 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 0d23bfb4ac8b1..725bbbd87c734 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -60,7 +60,7 @@ const ( // MagicSSHSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection. // This is stripped from any commands being executed, and is counted towards connection stats. - MagicSSHSessionTypeEnvironmentVariable = "__CODER_SSH_SESSION_TYPE" + MagicSSHSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE" // MagicSSHSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself. MagicSSHSessionTypeVSCode = "vscode" // MagicSSHSessionTypeJetBrains is set in the SSH config by the JetBrains extension to identify itself. @@ -156,10 +156,6 @@ type agent struct { network *tailnet.Conn connStatsChan chan *agentsdk.Stats - statRxPackets atomic.Int64 - statRxBytes atomic.Int64 - statTxPackets atomic.Int64 - statTxBytes atomic.Int64 connCountVSCode atomic.Int64 connCountJetBrains atomic.Int64 connCountReconnectingPTY atomic.Int64 @@ -1188,14 +1184,12 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { ConnectionCount: int64(len(networkStats)), ConnectionsByProto: map[string]int64{}, } - // Tailscale resets counts on every report! - // We'd rather have these compound, like Linux does! for conn, counts := range networkStats { stats.ConnectionsByProto[conn.Proto.String()]++ - stats.RxBytes = a.statRxBytes.Add(int64(counts.RxBytes)) - stats.RxPackets = a.statRxPackets.Add(int64(counts.RxPackets)) - stats.TxBytes = a.statTxBytes.Add(int64(counts.TxBytes)) - stats.TxPackets = a.statTxPackets.Add(int64(counts.TxPackets)) + stats.RxBytes += int64(counts.RxBytes) + stats.RxPackets += int64(counts.RxPackets) + stats.TxBytes += int64(counts.TxBytes) + stats.TxPackets += int64(counts.TxPackets) } // The count of active sessions. diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index b81ef3cb8972d..83fd42527d7e4 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -263,6 +263,51 @@ func (*fakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { return nil } +func (q *fakeQuerier) GetDeploymentWorkspaceAgentStats(_ context.Context, createdAfter time.Time) (database.GetDeploymentWorkspaceAgentStatsRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0) + for _, agentStat := range q.workspaceAgentStats { + if agentStat.CreatedAt.After(createdAfter) { + agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat) + } + } + + latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} + for _, agentStat := range q.workspaceAgentStats { + latestAgentStats[agentStat.AgentID] = agentStat + } + + stat := database.GetDeploymentWorkspaceAgentStatsRow{} + for _, agentStat := range latestAgentStats { + stat.SessionCountVSCode += agentStat.SessionCountVSCode + stat.SessionCountJetBrains += agentStat.SessionCountJetBrains + stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY + stat.SessionCountSSH += agentStat.SessionCountSSH + } + + latencies := make([]float64, 0) + for _, agentStat := range agentStatsCreatedAfter { + stat.WorkspaceRxBytes += agentStat.RxBytes + stat.WorkspaceTxBytes += agentStat.TxBytes + latencies = append(latencies, float64(agentStat.ConnectionMedianLatencyMS)) + } + + tryPercentile := func(fs []float64, p float64) float64 { + if len(fs) == 0 { + return -1 + } + sort.Float64s(fs) + return fs[int(float64(len(fs))*p/100)] + } + + stat.WorkspaceConnectionLatency50 = tryPercentile(latencies, 50) + stat.WorkspaceConnectionLatency95 = tryPercentile(latencies, 95) + + return stat, nil +} + func (q *fakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) { if err := validateDatabaseType(p); err != nil { return database.WorkspaceAgentStat{}, err diff --git a/coderd/database/generate.sh b/coderd/database/generate.sh index e70bbae939ff6..5669f121a57e0 100755 --- a/coderd/database/generate.sh +++ b/coderd/database/generate.sh @@ -18,7 +18,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") # Dump the updated schema (use make to utilize caching). make -C ../.. --no-print-directory coderd/database/dump.sql # The logic below depends on the exact version being correct :( - go run github.com/kyleconroy/sqlc/cmd/sqlc@v1.16.0 generate + sqlc generate first=true for fi in queries/*.sql.go; do diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 5082f1e9dfa23..b989fd44d0471 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -43,6 +43,7 @@ type sqlcQuerier interface { GetDERPMeshKey(ctx context.Context) (string, error) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error) GetDeploymentID(ctx context.Context) (string, error) + GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) // This will never count deleted users. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 60541499003a0..1b0f1b7a63bda 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5360,6 +5360,52 @@ func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUs return items, nil } +const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one +WITH agent_stats AS ( + SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh FROM workspace_agent_stats + WHERE created_at > $1 +), latest_agent_stats AS ( + SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh FROM agent_stats GROUP BY agent_id ORDER BY created_at +) +SELECT + SUM(latest_agent_stats.session_count_vscode) AS session_count_vscode, + SUM(latest_agent_stats.session_count_ssh) AS session_count_ssh, + SUM(latest_agent_stats.session_count_jetbrains) AS session_count_jetbrains, + SUM(latest_agent_stats.session_count_reconnecting_pty) AS session_count_reconnecting_pty, + SUM(agent_stats.rx_bytes) AS workspace_rx_bytes, + SUM(agent_stats.tx_bytes) AS workspace_tx_bytes, + coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 + FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id +` + +type GetDeploymentWorkspaceAgentStatsRow struct { + SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"` + SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"` + SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` + SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` + WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"` + WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"` + WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"` + WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"` +} + +func (q *sqlQuerier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) { + row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentStats, createdAt) + var i GetDeploymentWorkspaceAgentStatsRow + err := row.Scan( + &i.SessionCountVSCode, + &i.SessionCountSSH, + &i.SessionCountJetBrains, + &i.SessionCountReconnectingPTY, + &i.WorkspaceRxBytes, + &i.WorkspaceTxBytes, + &i.WorkspaceConnectionLatency50, + &i.WorkspaceConnectionLatency95, + ) + return i, err +} + const getTemplateDAUs = `-- name: GetTemplateDAUs :many SELECT (created_at at TIME ZONE 'UTC')::date as date, diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 57225711bde7e..43e927cb9636a 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -48,3 +48,21 @@ ORDER BY -- name: DeleteOldWorkspaceAgentStats :exec DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'; + +-- name: GetDeploymentWorkspaceAgentStats :one +WITH agent_stats AS ( + SELECT * FROM workspace_agent_stats + WHERE created_at > $1 +), latest_agent_stats AS ( + SELECT * FROM agent_stats GROUP BY agent_id ORDER BY created_at +) +SELECT + SUM(latest_agent_stats.session_count_vscode) AS session_count_vscode, + SUM(latest_agent_stats.session_count_ssh) AS session_count_ssh, + SUM(latest_agent_stats.session_count_jetbrains) AS session_count_jetbrains, + SUM(latest_agent_stats.session_count_reconnecting_pty) AS session_count_reconnecting_pty, + SUM(agent_stats.rx_bytes) AS workspace_rx_bytes, + SUM(agent_stats.tx_bytes) AS workspace_tx_bytes, + coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 + FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id; diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 7fb80303e6b56..437878a88f71a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -528,3 +528,24 @@ func (c *Client) AppHost(ctx context.Context) (AppHostResponse, error) { var host AppHostResponse return host, json.NewDecoder(res.Body).Decode(&host) } + +type WorkspaceConnectionLatency struct { + P50 *int64 + P95 *int64 +} + +type DeploymentStats struct { + WorkspacesByTransition map[WorkspaceTransition]int + WorkspaceConnectionLatencyMS WorkspaceConnectionLatency + + SessionCountVSCode int + SessionCountSSH int + SessionCountJetBrains int + SessionCountReconnectingPTY int + + WorkspaceRxBytes int + WorkspaceTxBytes int + + // X downloaded in the past 10 minutes + // X uploaded in the past 10 minutes +} From 28d6db5ab0db8913d2dc0850a476cabc62023ed6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Mar 2023 21:31:39 +0000 Subject: [PATCH 03/24] Add deployment stats endpoint --- cli/deployment/config.go | 2 +- coderd/apidoc/docs.go | 124 ++++++++++++++---- coderd/apidoc/swagger.json | 112 +++++++++++++--- coderd/coderd.go | 9 +- coderd/database/dbauthz/system.go | 4 + coderd/database/dbfake/databasefake.go | 4 +- coderd/deployment.go | 49 +++++++ ...ymentconfig_test.go => deployment_test.go} | 10 ++ coderd/deploymentconfig.go | 24 ---- coderd/metricscache/metricscache.go | 29 ++++ coderd/metricscache/metricscache_test.go | 25 ++++ coderd/rbac/object.go | 4 + codersdk/deployment.go | 43 ++++-- docs/api/general.md | 80 ++++++++--- docs/api/schemas.md | 55 ++++++++ site/src/api/api.ts | 8 +- site/src/api/typesGenerated.ts | 20 +++ 17 files changed, 494 insertions(+), 108 deletions(-) create mode 100644 coderd/deployment.go rename coderd/{deploymentconfig_test.go => deployment_test.go} (78%) delete mode 100644 coderd/deploymentconfig.go diff --git a/cli/deployment/config.go b/cli/deployment/config.go index d265ae67eeffe..824a0286e2210 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -406,7 +406,7 @@ func newConfig() *codersdk.DeploymentConfig { Usage: "How frequently agent stats are recorded", Flag: "agent-stats-refresh-interval", Hidden: true, - Default: 10 * time.Minute, + Default: time.Minute, }, AgentFallbackTroubleshootingURL: &codersdk.DeploymentConfigField[string]{ Name: "Agent Fallback Troubleshooting URL", diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d0ec2b075f9da..87971463e325a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -304,31 +304,6 @@ const docTemplate = `{ } } }, - "/config/deployment": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "General" - ], - "summary": "Get deployment config", - "operationId": "get-deployment-config", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.DeploymentConfig" - } - } - } - } - }, "/csp/reports": { "post": { "security": [ @@ -384,6 +359,56 @@ const docTemplate = `{ } } }, + "/deployment/config": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get deployment config", + "operationId": "get-deployment-config", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.DeploymentConfig" + } + } + } + } + }, + "/deployment/stats": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get deployment stats", + "operationId": "get-deployment-stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.DeploymentStats" + } + } + } + } + }, "/entitlements": { "get": { "security": [ @@ -6596,6 +6621,44 @@ const docTemplate = `{ } } }, + "codersdk.DeploymentStats": { + "type": "object", + "properties": { + "aggregated_from": { + "type": "string" + }, + "session_count_jetbrains": { + "type": "integer" + }, + "session_count_reconnecting_pty": { + "type": "integer" + }, + "session_count_ssh": { + "type": "integer" + }, + "session_count_vscode": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace_connection_latency_ms": { + "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" + }, + "workspace_rx_bytes": { + "type": "integer" + }, + "workspace_tx_bytes": { + "type": "integer" + }, + "workspaces_by_transition": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, "codersdk.Entitlement": { "type": "string", "enum": [ @@ -8671,6 +8734,17 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceConnectionLatencyMS": { + "type": "object", + "properties": { + "p50": { + "type": "number" + }, + "p95": { + "type": "number" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0bc82ed8ee9ba..ea5943f670b03 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -258,27 +258,6 @@ } } }, - "/config/deployment": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["General"], - "summary": "Get deployment config", - "operationId": "get-deployment-config", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.DeploymentConfig" - } - } - } - } - }, "/csp/reports": { "post": { "security": [ @@ -326,6 +305,48 @@ } } }, + "/deployment/config": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["General"], + "summary": "Get deployment config", + "operationId": "get-deployment-config", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.DeploymentConfig" + } + } + } + } + }, + "/deployment/stats": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["General"], + "summary": "Get deployment stats", + "operationId": "get-deployment-stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.DeploymentStats" + } + } + } + } + }, "/entitlements": { "get": { "security": [ @@ -5904,6 +5925,44 @@ } } }, + "codersdk.DeploymentStats": { + "type": "object", + "properties": { + "aggregated_from": { + "type": "string" + }, + "session_count_jetbrains": { + "type": "integer" + }, + "session_count_reconnecting_pty": { + "type": "integer" + }, + "session_count_ssh": { + "type": "integer" + }, + "session_count_vscode": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "workspace_connection_latency_ms": { + "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" + }, + "workspace_rx_bytes": { + "type": "integer" + }, + "workspace_tx_bytes": { + "type": "integer" + }, + "workspaces_by_transition": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, "codersdk.Entitlement": { "type": "string", "enum": ["entitled", "grace_period", "not_entitled"], @@ -7810,6 +7869,17 @@ } } }, + "codersdk.WorkspaceConnectionLatencyMS": { + "type": "object", + "properties": { + "p50": { + "type": "number" + }, + "p95": { + "type": "number" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 276282d6c6800..1b5a841135e11 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -398,15 +398,16 @@ func New(options *Options) *API { r.Post("/csp/reports", api.logReportCSPViolations) r.Get("/buildinfo", buildInfo) + r.Route("/deployment", func(r chi.Router) { + r.Use(apiKeyMiddleware) + r.Get("/config", api.deploymentConfig) + r.Get("/stats", api.deploymentStats) + }) r.Route("/experiments", func(r chi.Router) { r.Use(apiKeyMiddleware) r.Get("/", api.handleExperimentsGet) }) r.Get("/updatecheck", api.updateCheck) - r.Route("/config", func(r chi.Router) { - r.Use(apiKeyMiddleware) - r.Get("/deployment", api.deploymentConfig) - }) r.Route("/audit", func(r chi.Router) { r.Use( apiKeyMiddleware, diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index b290f68bc8428..8a12442e5cae3 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -201,6 +201,10 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { return q.db.DeleteOldWorkspaceAgentStats(ctx) } +func (q *querier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAfter time.Time) (database.GetDeploymentWorkspaceAgentStatsRow, error) { + return q.db.GetDeploymentWorkspaceAgentStats(ctx, createdAfter) +} + func (q *querier) GetParameterSchemasCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ParameterSchema, error) { return q.db.GetParameterSchemasCreatedAfter(ctx, createdAt) } diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 83fd42527d7e4..99f1668987545 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -276,7 +276,9 @@ func (q *fakeQuerier) GetDeploymentWorkspaceAgentStats(_ context.Context, create latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{} for _, agentStat := range q.workspaceAgentStats { - latestAgentStats[agentStat.AgentID] = agentStat + if agentStat.CreatedAt.After(createdAfter) { + latestAgentStats[agentStat.AgentID] = agentStat + } } stat := database.GetDeploymentWorkspaceAgentStatsRow{} diff --git a/coderd/deployment.go b/coderd/deployment.go new file mode 100644 index 0000000000000..9af5b1a416f94 --- /dev/null +++ b/coderd/deployment.go @@ -0,0 +1,49 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/rbac" + "github.com/coder/coder/codersdk" +) + +// @Summary Get deployment config +// @ID get-deployment-config +// @Security CoderSessionToken +// @Produce json +// @Tags General +// @Success 200 {object} codersdk.DeploymentConfig +// @Router /deployment/config [get] +func (api *API) deploymentConfig(rw http.ResponseWriter, r *http.Request) { + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) { + httpapi.Forbidden(rw) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, api.DeploymentConfig) +} + +// @Summary Get deployment stats +// @ID get-deployment-stats +// @Security CoderSessionToken +// @Produce json +// @Tags General +// @Success 200 {object} codersdk.DeploymentStats +// @Router /deployment/stats [get] +func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) { + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) { + httpapi.Forbidden(rw) + return + } + + stats, ok := api.metricsCache.DeploymentStats() + if !ok { + httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{ + Message: "Deployment stats are still processing!", + }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, stats) +} diff --git a/coderd/deploymentconfig_test.go b/coderd/deployment_test.go similarity index 78% rename from coderd/deploymentconfig_test.go rename to coderd/deployment_test.go index 5389151d2dee6..14cef6f1925f6 100644 --- a/coderd/deploymentconfig_test.go +++ b/coderd/deployment_test.go @@ -38,3 +38,13 @@ func TestDeploymentConfig(t *testing.T) { require.Empty(t, scrubbed.PostgresURL.Value) require.Empty(t, scrubbed.SCIMAPIKey.Value) } + +func TestDeploymentStats(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + client := coderdtest.New(t, &coderdtest.Options{}) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.DeploymentStats(ctx) + require.NoError(t, err) +} diff --git a/coderd/deploymentconfig.go b/coderd/deploymentconfig.go deleted file mode 100644 index 9b84120d77af5..0000000000000 --- a/coderd/deploymentconfig.go +++ /dev/null @@ -1,24 +0,0 @@ -package coderd - -import ( - "net/http" - - "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/coderd/rbac" -) - -// @Summary Get deployment config -// @ID get-deployment-config -// @Security CoderSessionToken -// @Produce json -// @Tags General -// @Success 200 {object} codersdk.DeploymentConfig -// @Router /config/deployment [get] -func (api *API) deploymentConfig(rw http.ResponseWriter, r *http.Request) { - if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDeploymentConfig) { - httpapi.Forbidden(rw) - return - } - - httpapi.Write(r.Context(), rw, http.StatusOK, api.DeploymentConfig) -} diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 176315afc7172..19db148a258d8 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -32,6 +32,7 @@ type Cache struct { templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse] templateUniqueUsers atomic.Pointer[map[uuid.UUID]int] templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow] + deploymentStatsResponse atomic.Pointer[codersdk.DeploymentStats] done chan struct{} cancel func() @@ -196,6 +197,26 @@ func (c *Cache) refresh(ctx context.Context) error { c.templateUniqueUsers.Store(&templateUniqueUsers) c.templateAverageBuildTime.Store(&templateAverageBuildTimes) + from := database.Now().Add(-15 * time.Minute) + deploymentStats, err := c.database.GetDeploymentWorkspaceAgentStats(ctx, from) + if err != nil { + return err + } + c.deploymentStatsResponse.Store(&codersdk.DeploymentStats{ + AggregatedFrom: from, + UpdatedAt: database.Now(), + WorkspaceConnectionLatencyMS: codersdk.WorkspaceConnectionLatencyMS{ + P50: deploymentStats.WorkspaceConnectionLatency50, + P95: deploymentStats.WorkspaceConnectionLatency95, + }, + SessionCountVSCode: deploymentStats.SessionCountVSCode, + SessionCountSSH: deploymentStats.SessionCountSSH, + SessionCountJetBrains: deploymentStats.SessionCountJetBrains, + SessionCountReconnectingPTY: deploymentStats.SessionCountReconnectingPTY, + WorkspaceRxBytes: deploymentStats.WorkspaceRxBytes, + WorkspaceTxBytes: deploymentStats.WorkspaceTxBytes, + }) + return nil } @@ -322,3 +343,11 @@ func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeS }, } } + +func (c *Cache) DeploymentStats() (codersdk.DeploymentStats, bool) { + deploymentStats := c.deploymentStatsResponse.Load() + if deploymentStats == nil { + return codersdk.DeploymentStats{}, false + } + return *deploymentStats, true +} diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 98190ac4f4935..9be1d9ba145d9 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -373,3 +373,28 @@ func TestCache_BuildTime(t *testing.T) { }) } } + +func TestCache_DeploymentStats(t *testing.T) { + t.Parallel() + db := dbfake.New() + cache := metricscache.New(db, slogtest.Make(t, nil), testutil.IntervalFast) + + _, err := db.InsertWorkspaceAgentStat(context.Background(), database.InsertWorkspaceAgentStatParams{ + ID: uuid.New(), + AgentID: uuid.New(), + CreatedAt: database.Now(), + ConnectionCount: 1, + RxBytes: 1, + TxBytes: 1, + SessionCountVSCode: 1, + }) + require.NoError(t, err) + + var stat codersdk.DeploymentStats + require.Eventually(t, func() bool { + var ok bool + stat, ok = cache.DeploymentStats() + return ok + }, testutil.WaitLong, testutil.IntervalMedium) + require.Equal(t, int64(1), stat.SessionCountVSCode) +} diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 8f71ee3419566..543465d430563 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -147,6 +147,10 @@ var ( Type: "deployment_config", } + ResourceDeploymentStats = Object{ + Type: "deployment_stats", + } + ResourceReplicas = Object{ Type: "replicas", } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 437878a88f71a..2a991140db32a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -343,7 +343,7 @@ func (f *DeploymentConfigField[T]) MarshalJSON() ([]byte, error) { // DeploymentConfig returns the deployment config for the coder server. func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/config/deployment", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/deployment/config", nil) if err != nil { return DeploymentConfig{}, xerrors.Errorf("execute request: %w", err) } @@ -357,6 +357,21 @@ func (c *Client) DeploymentConfig(ctx context.Context) (DeploymentConfig, error) return df, json.NewDecoder(res.Body).Decode(&df) } +func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/deployment/stats", nil) + if err != nil { + return DeploymentStats{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return DeploymentStats{}, ReadBodyAsError(res) + } + + var df DeploymentStats + return df, json.NewDecoder(res.Body).Decode(&df) +} + type AppearanceConfig struct { LogoURL string `json:"logo_url"` ServiceBanner ServiceBannerConfig `json:"service_banner"` @@ -529,23 +544,23 @@ func (c *Client) AppHost(ctx context.Context) (AppHostResponse, error) { return host, json.NewDecoder(res.Body).Decode(&host) } -type WorkspaceConnectionLatency struct { - P50 *int64 - P95 *int64 +type WorkspaceConnectionLatencyMS struct { + P50 float64 + P95 float64 } type DeploymentStats struct { - WorkspacesByTransition map[WorkspaceTransition]int - WorkspaceConnectionLatencyMS WorkspaceConnectionLatency + AggregatedFrom time.Time `json:"aggregated_from"` + UpdatedAt time.Time `json:"updated_at"` - SessionCountVSCode int - SessionCountSSH int - SessionCountJetBrains int - SessionCountReconnectingPTY int + WorkspacesByTransition map[WorkspaceTransition]int `json:"workspaces_by_transition"` + WorkspaceConnectionLatencyMS WorkspaceConnectionLatencyMS `json:"workspace_connection_latency_ms"` - WorkspaceRxBytes int - WorkspaceTxBytes int + SessionCountVSCode int64 `json:"session_count_vscode"` + SessionCountSSH int64 `json:"session_count_ssh"` + SessionCountJetBrains int64 `json:"session_count_jetbrains"` + SessionCountReconnectingPTY int64 `json:"session_count_reconnecting_pty"` - // X downloaded in the past 10 minutes - // X uploaded in the past 10 minutes + WorkspaceRxBytes int64 `json:"workspace_rx_bytes"` + WorkspaceTxBytes int64 `json:"workspace_tx_bytes"` } diff --git a/docs/api/general.md b/docs/api/general.md index 8102f24af5df1..881ae443e1c9d 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -64,18 +64,53 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \ | ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------ | | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.BuildInfoResponse](schemas.md#codersdkbuildinforesponse) | +## Report CSP violations + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/csp/reports \ + -H 'Content-Type: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /csp/reports` + +> Body parameter + +```json +{ + "csp-report": {} +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | ---------------------------------------------------- | -------- | ---------------- | +| `body` | body | [coderd.cspViolation](schemas.md#coderdcspviolation) | true | Violation report | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get deployment config ### Code samples ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/config/deployment \ +curl -X GET http://coder-server:8080/api/v2/deployment/config \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`GET /config/deployment` +`GET /deployment/config` ### Example responses @@ -1132,38 +1167,49 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Report CSP violations +## Get deployment stats ### Code samples ```shell # Example request using curl -curl -X POST http://coder-server:8080/api/v2/csp/reports \ - -H 'Content-Type: application/json' \ +curl -X GET http://coder-server:8080/api/v2/deployment/stats \ + -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`POST /csp/reports` +`GET /deployment/stats` -> Body parameter +### Example responses + +> 200 Response ```json { - "csp-report": {} + "aggregated_from": "string", + "session_count_jetbrains": 0, + "session_count_reconnecting_pty": 0, + "session_count_ssh": 0, + "session_count_vscode": 0, + "updated_at": "string", + "workspace_connection_latency_ms": { + "p50": 0, + "p95": 0 + }, + "workspace_rx_bytes": 0, + "workspace_tx_bytes": 0, + "workspaces_by_transition": { + "property1": 0, + "property2": 0 + } } ``` -### Parameters - -| Name | In | Type | Required | Description | -| ------ | ---- | ---------------------------------------------------- | -------- | ---------------- | -| `body` | body | [coderd.cspViolation](schemas.md#coderdcspviolation) | true | Violation report | - ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.DeploymentStats](schemas.md#codersdkdeploymentstats) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 7385b1b61f7ac..141457c971596 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2917,6 +2917,45 @@ CreateParameterRequest is a structure used to create a new parameter value for a | --------- | ----------------------------------------------- | -------- | ------------ | ----------- | | `entries` | array of [codersdk.DAUEntry](#codersdkdauentry) | false | | | +## codersdk.DeploymentStats + +```json +{ + "aggregated_from": "string", + "session_count_jetbrains": 0, + "session_count_reconnecting_pty": 0, + "session_count_ssh": 0, + "session_count_vscode": 0, + "updated_at": "string", + "workspace_connection_latency_ms": { + "p50": 0, + "p95": 0 + }, + "workspace_rx_bytes": 0, + "workspace_tx_bytes": 0, + "workspaces_by_transition": { + "property1": 0, + "property2": 0 + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `aggregated_from` | string | false | | | +| `session_count_jetbrains` | integer | false | | | +| `session_count_reconnecting_pty` | integer | false | | | +| `session_count_ssh` | integer | false | | | +| `session_count_vscode` | integer | false | | | +| `updated_at` | string | false | | | +| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | +| `workspace_rx_bytes` | integer | false | | | +| `workspace_tx_bytes` | integer | false | | | +| `workspaces_by_transition` | object | false | | | +| ยป `[any property]` | integer | false | | | + ## codersdk.Entitlement ```json @@ -5910,6 +5949,22 @@ Parameter represents a set value for the scope. | `name` | string | false | | | | `value` | string | false | | | +## codersdk.WorkspaceConnectionLatencyMS + +```json +{ + "p50": 0, + "p95": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----- | ------ | -------- | ------------ | ----------- | +| `p50` | number | false | | | +| `p95` | number | false | | | + ## codersdk.WorkspaceQuota ```json diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 1fa82a0ba86b8..b117861ed9849 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -808,7 +808,13 @@ export const getAgentListeningPorts = async ( export const getDeploymentConfig = async (): Promise => { - const response = await axios.get(`/api/v2/config/deployment`) + const response = await axios.get(`/api/v2/deployment/config`) + return response.data + } + +export const getDeploymentStats = + async (): Promise => { + const response = await axios.get(`/api/v2/deployment/stats`) return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cc2d5f95feab3..b4b723550abac 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -361,6 +361,20 @@ export interface DeploymentDAUsResponse { readonly entries: DAUEntry[] } +// From codersdk/deployment.go +export interface DeploymentStats { + readonly aggregated_from: string + readonly updated_at: string + readonly workspaces_by_transition: Record + readonly workspace_connection_latency_ms: WorkspaceConnectionLatencyMS + readonly session_count_vscode: number + readonly session_count_ssh: number + readonly session_count_jetbrains: number + readonly session_count_reconnecting_pty: number + readonly workspace_rx_bytes: number + readonly workspace_tx_bytes: number +} + // From codersdk/deployment.go export interface Entitlements { readonly features: Record @@ -1056,6 +1070,12 @@ export interface WorkspaceBuildsRequest extends Pagination { readonly Since: string } +// From codersdk/deployment.go +export interface WorkspaceConnectionLatencyMS { + readonly P50: number + readonly P95: number +} + // From codersdk/workspaces.go export interface WorkspaceFilter { readonly q?: string From 29719a48e7eba96891e3f3cd4accf2cb9c5e7a7c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Mar 2023 22:09:17 +0000 Subject: [PATCH 04/24] The query... works?!? --- coderd/database/queries.sql.go | 19 ++++++++++--------- .../database/queries/workspaceagentstats.sql | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 1b0f1b7a63bda..c17e023ec62f8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5363,20 +5363,21 @@ func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUs const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one WITH agent_stats AS ( SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh FROM workspace_agent_stats - WHERE created_at > $1 + WHERE workspace_agent_stats.created_at > $1 ), latest_agent_stats AS ( - SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh FROM agent_stats GROUP BY agent_id ORDER BY created_at + SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, ROW_NUMBER() OVER(PARTITION BY id ORDER BY created_at DESC) AS rn + FROM agent_stats ) SELECT - SUM(latest_agent_stats.session_count_vscode) AS session_count_vscode, - SUM(latest_agent_stats.session_count_ssh) AS session_count_ssh, - SUM(latest_agent_stats.session_count_jetbrains) AS session_count_jetbrains, - SUM(latest_agent_stats.session_count_reconnecting_pty) AS session_count_reconnecting_pty, - SUM(agent_stats.rx_bytes) AS workspace_rx_bytes, - SUM(agent_stats.tx_bytes) AS workspace_tx_bytes, + coalesce(SUM(latest_agent_stats.session_count_vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(latest_agent_stats.session_count_ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(latest_agent_stats.session_count_jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(latest_agent_stats.session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(agent_stats.rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(agent_stats.tx_bytes), 0)::bigint AS workspace_tx_bytes, coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 - FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id + FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id AND rn = 1 ` type GetDeploymentWorkspaceAgentStatsRow struct { diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 43e927cb9636a..f07fc1d14b3ab 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -52,17 +52,18 @@ DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'; -- name: GetDeploymentWorkspaceAgentStats :one WITH agent_stats AS ( SELECT * FROM workspace_agent_stats - WHERE created_at > $1 + WHERE workspace_agent_stats.created_at > $1 ), latest_agent_stats AS ( - SELECT * FROM agent_stats GROUP BY agent_id ORDER BY created_at + SELECT *, ROW_NUMBER() OVER(PARTITION BY id ORDER BY created_at DESC) AS rn + FROM agent_stats ) SELECT - SUM(latest_agent_stats.session_count_vscode) AS session_count_vscode, - SUM(latest_agent_stats.session_count_ssh) AS session_count_ssh, - SUM(latest_agent_stats.session_count_jetbrains) AS session_count_jetbrains, - SUM(latest_agent_stats.session_count_reconnecting_pty) AS session_count_reconnecting_pty, - SUM(agent_stats.rx_bytes) AS workspace_rx_bytes, - SUM(agent_stats.tx_bytes) AS workspace_tx_bytes, + coalesce(SUM(latest_agent_stats.session_count_vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(latest_agent_stats.session_count_ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(latest_agent_stats.session_count_jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(latest_agent_stats.session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, + coalesce(SUM(agent_stats.rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(agent_stats.tx_bytes), 0)::bigint AS workspace_tx_bytes, coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 - FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id; + FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id AND rn = 1; From 09a2dadaf0f6839cd3730d401148a4063a70881b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 6 Mar 2023 18:08:47 +0000 Subject: [PATCH 05/24] Fix aggregation query --- coderd/coderd.go | 5 +- coderd/database/dbgen/generator.go | 28 ++++++ coderd/database/dump.sql | 2 +- .../000102_workspace_agent_stats_types.up.sql | 2 +- coderd/database/models.go | 2 +- coderd/database/querier_test.go | 86 +++++++++++++++++++ coderd/database/queries.sql.go | 47 +++++----- .../database/queries/workspaceagentstats.sql | 29 ++++--- coderd/metricscache/metricscache.go | 49 ++++++++--- coderd/metricscache/metricscache_test.go | 12 ++- coderd/workspaceagents.go | 2 +- 11 files changed, 207 insertions(+), 57 deletions(-) create mode 100644 coderd/database/querier_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 1b5a841135e11..b8787a090ea3f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -222,7 +222,10 @@ func New(options *Options) *API { metricsCache := metricscache.New( options.Database, options.Logger.Named("metrics_cache"), - options.MetricsCacheRefreshInterval, + metricscache.Intervals{ + TemplateDAUs: options.MetricsCacheRefreshInterval, + DeploymentStats: options.AgentStatsRefreshInterval, + }, ) staticHandler := site.Handler(site.FS(), binFS, binHashes) diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index cb8f52c06529a..c253c58533312 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "database/sql" "encoding/hex" + "encoding/json" "fmt" "net" "testing" @@ -437,3 +438,30 @@ func ParameterValue(t testing.TB, db database.Store, seed database.ParameterValu require.NoError(t, err, "insert parameter value") return scheme } + +func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.WorkspaceAgentStat) database.WorkspaceAgentStat { + if orig.ConnectionsByProto == nil { + orig.ConnectionsByProto = json.RawMessage([]byte("{}")) + } + scheme, err := db.InsertWorkspaceAgentStat(context.Background(), database.InsertWorkspaceAgentStatParams{ + ID: takeFirst(orig.ID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, database.Now()), + UserID: takeFirst(orig.UserID, uuid.New()), + TemplateID: takeFirst(orig.TemplateID, uuid.New()), + WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), + AgentID: takeFirst(orig.AgentID, uuid.New()), + ConnectionsByProto: orig.ConnectionsByProto, + ConnectionCount: takeFirst(orig.ConnectionCount, 0), + RxPackets: takeFirst(orig.RxPackets, 0), + RxBytes: takeFirst(orig.RxBytes, 0), + TxPackets: takeFirst(orig.TxPackets, 0), + TxBytes: takeFirst(orig.TxBytes, 0), + SessionCountVSCode: takeFirst(orig.SessionCountVSCode, 0), + SessionCountJetBrains: takeFirst(orig.SessionCountJetBrains, 0), + SessionCountReconnectingPTY: takeFirst(orig.SessionCountReconnectingPTY, 0), + SessionCountSSH: takeFirst(orig.SessionCountSSH, 0), + ConnectionMedianLatencyMS: takeFirst(orig.ConnectionMedianLatencyMS, 0), + }) + require.NoError(t, err, "insert workspace agent stat") + return scheme +} diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index bccf259df243a..44c837f99421a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -475,7 +475,7 @@ CREATE TABLE workspace_agent_stats ( rx_bytes bigint DEFAULT 0 NOT NULL, tx_packets bigint DEFAULT 0 NOT NULL, tx_bytes bigint DEFAULT 0 NOT NULL, - connection_median_latency_ms bigint DEFAULT '-1'::integer NOT NULL, + connection_median_latency_ms double precision DEFAULT '-1'::integer NOT NULL, session_count_vscode bigint DEFAULT 0 NOT NULL, session_count_jetbrains bigint DEFAULT 0 NOT NULL, session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL, diff --git a/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql b/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql index 39effadfee42c..421d5478e977a 100644 --- a/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql +++ b/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql @@ -1,4 +1,4 @@ -ALTER TABLE workspace_agent_stats ADD COLUMN connection_median_latency_ms bigint DEFAULT -1 NOT NULL; +ALTER TABLE workspace_agent_stats ADD COLUMN connection_median_latency_ms float DEFAULT -1 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_vscode bigint DEFAULT 0 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_jetbrains bigint DEFAULT 0 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL; diff --git a/coderd/database/models.go b/coderd/database/models.go index 9dfc6671716ba..289cea66e9326 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1561,7 +1561,7 @@ type WorkspaceAgentStat struct { RxBytes int64 `db:"rx_bytes" json:"rx_bytes"` TxPackets int64 `db:"tx_packets" json:"tx_packets"` TxBytes int64 `db:"tx_bytes" json:"tx_bytes"` - ConnectionMedianLatencyMS int64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` + ConnectionMedianLatencyMS float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"` SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go new file mode 100644 index 0000000000000..8ed967e8fcf52 --- /dev/null +++ b/coderd/database/querier_test.go @@ -0,0 +1,86 @@ +package database_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbgen" + "github.com/coder/coder/coderd/database/migrations" +) + +func TestGetDeploymentWorkspaceAgentStats(t *testing.T) { + t.Parallel() + if testing.Short() { + t.SkipNow() + } + t.Run("Aggregates", func(t *testing.T) { + t.Parallel() + sqlDB := testSQLDB(t) + err := migrations.Up(sqlDB) + require.NoError(t, err) + db := database.New(sqlDB) + ctx := context.Background() + dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ + TxBytes: 1, + RxBytes: 1, + ConnectionMedianLatencyMS: 1, + SessionCountVSCode: 1, + }) + dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ + TxBytes: 1, + RxBytes: 1, + ConnectionMedianLatencyMS: 2, + SessionCountVSCode: 1, + }) + stats, err := db.GetDeploymentWorkspaceAgentStats(ctx, database.Now().Add(-time.Hour)) + require.NoError(t, err) + + require.Equal(t, int64(2), stats.WorkspaceTxBytes) + require.Equal(t, int64(2), stats.WorkspaceRxBytes) + require.Equal(t, 1.5, stats.WorkspaceConnectionLatency50) + require.Equal(t, 1.95, stats.WorkspaceConnectionLatency95) + require.Equal(t, int64(2), stats.SessionCountVSCode) + }) + + t.Run("GroupsByAgentID", func(t *testing.T) { + t.Parallel() + + sqlDB := testSQLDB(t) + err := migrations.Up(sqlDB) + require.NoError(t, err) + db := database.New(sqlDB) + ctx := context.Background() + agentID := uuid.New() + insertTime := database.Now() + dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ + CreatedAt: insertTime.Add(-time.Second), + AgentID: agentID, + TxBytes: 1, + RxBytes: 1, + ConnectionMedianLatencyMS: 1, + SessionCountVSCode: 1, + }) + dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{ + // Ensure this stat is newer! + CreatedAt: insertTime, + AgentID: agentID, + TxBytes: 1, + RxBytes: 1, + ConnectionMedianLatencyMS: 2, + SessionCountVSCode: 1, + }) + stats, err := db.GetDeploymentWorkspaceAgentStats(ctx, database.Now().Add(-time.Hour)) + require.NoError(t, err) + + require.Equal(t, int64(2), stats.WorkspaceTxBytes) + require.Equal(t, int64(2), stats.WorkspaceRxBytes) + require.Equal(t, 1.5, stats.WorkspaceConnectionLatency50) + require.Equal(t, 1.95, stats.WorkspaceConnectionLatency95) + require.Equal(t, int64(1), stats.SessionCountVSCode) + }) +} diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c17e023ec62f8..931671adbf099 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5362,47 +5362,50 @@ func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUs const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one WITH agent_stats AS ( - SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh FROM workspace_agent_stats + SELECT + coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, + coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 + FROM workspace_agent_stats WHERE workspace_agent_stats.created_at > $1 ), latest_agent_stats AS ( - SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, ROW_NUMBER() OVER(PARTITION BY id ORDER BY created_at DESC) AS rn - FROM agent_stats + SELECT + coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + FROM ( + SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn + FROM workspace_agent_stats + ) AS a WHERE a.rn = 1 ) -SELECT - coalesce(SUM(latest_agent_stats.session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(latest_agent_stats.session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(latest_agent_stats.session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(latest_agent_stats.session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(agent_stats.rx_bytes), 0)::bigint AS workspace_rx_bytes, - coalesce(SUM(agent_stats.tx_bytes), 0)::bigint AS workspace_tx_bytes, - coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, - coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 - FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id AND rn = 1 +SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats CROSS JOIN latest_agent_stats ` type GetDeploymentWorkspaceAgentStatsRow struct { - SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"` - SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"` - SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` - SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"` WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"` WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"` WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"` + SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"` + SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"` + SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` + SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` } func (q *sqlQuerier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) { row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentStats, createdAt) var i GetDeploymentWorkspaceAgentStatsRow err := row.Scan( - &i.SessionCountVSCode, - &i.SessionCountSSH, - &i.SessionCountJetBrains, - &i.SessionCountReconnectingPTY, &i.WorkspaceRxBytes, &i.WorkspaceTxBytes, &i.WorkspaceConnectionLatency50, &i.WorkspaceConnectionLatency95, + &i.SessionCountVSCode, + &i.SessionCountSSH, + &i.SessionCountJetBrains, + &i.SessionCountReconnectingPTY, ) return i, err } @@ -5491,7 +5494,7 @@ type InsertWorkspaceAgentStatParams struct { SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"` SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"` SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"` - ConnectionMedianLatencyMS int64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` + ConnectionMedianLatencyMS float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"` } func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) { diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index f07fc1d14b3ab..18e5db776dbd0 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -51,19 +51,22 @@ DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'; -- name: GetDeploymentWorkspaceAgentStats :one WITH agent_stats AS ( - SELECT * FROM workspace_agent_stats + SELECT + coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes, + coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes, + coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, + coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 + FROM workspace_agent_stats WHERE workspace_agent_stats.created_at > $1 ), latest_agent_stats AS ( - SELECT *, ROW_NUMBER() OVER(PARTITION BY id ORDER BY created_at DESC) AS rn - FROM agent_stats + SELECT + coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode, + coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh, + coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains, + coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty + FROM ( + SELECT *, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn + FROM workspace_agent_stats + ) AS a WHERE a.rn = 1 ) -SELECT - coalesce(SUM(latest_agent_stats.session_count_vscode), 0)::bigint AS session_count_vscode, - coalesce(SUM(latest_agent_stats.session_count_ssh), 0)::bigint AS session_count_ssh, - coalesce(SUM(latest_agent_stats.session_count_jetbrains), 0)::bigint AS session_count_jetbrains, - coalesce(SUM(latest_agent_stats.session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty, - coalesce(SUM(agent_stats.rx_bytes), 0)::bigint AS workspace_rx_bytes, - coalesce(SUM(agent_stats.tx_bytes), 0)::bigint AS workspace_tx_bytes, - coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50, - coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY agent_stats.connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95 - FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id AND rn = 1; +SELECT * FROM agent_stats CROSS JOIN latest_agent_stats; diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 19db148a258d8..57b2194425b75 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -3,6 +3,7 @@ package metricscache import ( "context" "database/sql" + "sync" "sync/atomic" "time" @@ -36,13 +37,19 @@ type Cache struct { done chan struct{} cancel func() +} - interval time.Duration +type Intervals struct { + TemplateDAUs time.Duration + DeploymentStats time.Duration } -func New(db database.Store, log slog.Logger, interval time.Duration) *Cache { - if interval <= 0 { - interval = time.Hour +func New(db database.Store, log slog.Logger, intervals Intervals) *Cache { + if intervals.TemplateDAUs <= 0 { + intervals.TemplateDAUs = time.Hour + } + if intervals.DeploymentStats <= 0 { + intervals.DeploymentStats = time.Minute } ctx, cancel := context.WithCancel(context.Background()) @@ -51,9 +58,22 @@ func New(db database.Store, log slog.Logger, interval time.Duration) *Cache { log: log, done: make(chan struct{}), cancel: cancel, - interval: interval, } - go c.run(ctx) + go func() { + var wg sync.WaitGroup + defer close(c.done) + wg.Add(1) + go func() { + wg.Done() + c.run(ctx, intervals.TemplateDAUs, c.refreshTemplateDAUs) + }() + wg.Add(1) + go func() { + wg.Done() + c.run(ctx, intervals.DeploymentStats, c.refreshDeploymentStats) + }() + wg.Wait() + }() return c } @@ -143,7 +163,7 @@ func countUniqueUsers(rows []database.GetTemplateDAUsRow) int { return len(seen) } -func (c *Cache) refresh(ctx context.Context) error { +func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { //nolint:gocritic // This is a system service. ctx = dbauthz.AsSystemRestricted(ctx) err := c.database.DeleteOldWorkspaceAgentStats(ctx) @@ -197,6 +217,10 @@ func (c *Cache) refresh(ctx context.Context) error { c.templateUniqueUsers.Store(&templateUniqueUsers) c.templateAverageBuildTime.Store(&templateAverageBuildTimes) + return nil +} + +func (c *Cache) refreshDeploymentStats(ctx context.Context) error { from := database.Now().Add(-15 * time.Minute) deploymentStats, err := c.database.GetDeploymentWorkspaceAgentStats(ctx, from) if err != nil { @@ -216,20 +240,17 @@ func (c *Cache) refresh(ctx context.Context) error { WorkspaceRxBytes: deploymentStats.WorkspaceRxBytes, WorkspaceTxBytes: deploymentStats.WorkspaceTxBytes, }) - return nil } -func (c *Cache) run(ctx context.Context) { - defer close(c.done) - - ticker := time.NewTicker(c.interval) +func (c *Cache) run(ctx context.Context, interval time.Duration, refresh func(context.Context) error) { + ticker := time.NewTicker(interval) defer ticker.Stop() for { for r := retry.New(time.Millisecond*100, time.Minute); r.Wait(ctx); { start := time.Now() - err := c.refresh(ctx) + err := refresh(ctx) if err != nil { if ctx.Err() != nil { return @@ -241,7 +262,7 @@ func (c *Cache) run(ctx context.Context) { ctx, "metrics refreshed", slog.F("took", time.Since(start)), - slog.F("interval", c.interval), + slog.F("interval", interval), ) break } diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 9be1d9ba145d9..3ab9863911ca9 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -164,7 +164,9 @@ func TestCache_TemplateUsers(t *testing.T) { t.Parallel() var ( db = dbfake.New() - cache = metricscache.New(db, slogtest.Make(t, nil), testutil.IntervalFast) + cache = metricscache.New(db, slogtest.Make(t, nil), metricscache.Intervals{ + TemplateDAUs: testutil.IntervalFast, + }) ) defer cache.Close() @@ -289,7 +291,9 @@ func TestCache_BuildTime(t *testing.T) { var ( db = dbfake.New() - cache = metricscache.New(db, slogtest.Make(t, nil), testutil.IntervalFast) + cache = metricscache.New(db, slogtest.Make(t, nil), metricscache.Intervals{ + TemplateDAUs: testutil.IntervalFast, + }) ) defer cache.Close() @@ -377,7 +381,9 @@ func TestCache_BuildTime(t *testing.T) { func TestCache_DeploymentStats(t *testing.T) { t.Parallel() db := dbfake.New() - cache := metricscache.New(db, slogtest.Make(t, nil), testutil.IntervalFast) + cache := metricscache.New(db, slogtest.Make(t, nil), metricscache.Intervals{ + DeploymentStats: testutil.IntervalFast, + }) _, err := db.InsertWorkspaceAgentStat(context.Background(), database.InsertWorkspaceAgentStatParams{ ID: uuid.New(), diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 20202849aa47f..751aae97a60d3 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -957,7 +957,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques SessionCountJetBrains: req.SessionCountJetBrains, SessionCountReconnectingPTY: req.SessionCountReconnectingPTY, SessionCountSSH: req.SessionCountSSH, - ConnectionMedianLatencyMS: int64(req.ConnectionMedianLatencyMS), + ConnectionMedianLatencyMS: req.ConnectionMedianLatencyMS, }) if err != nil { httpapi.InternalServerError(rw, err) From 12a52b19596834d5ddd121a79f5c9ded38e9fc12 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 6 Mar 2023 18:09:45 +0000 Subject: [PATCH 06/24] Select from multiple tables instead --- coderd/database/queries.sql.go | 2 +- coderd/database/queries/workspaceagentstats.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 931671adbf099..10eca8b130347 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5380,7 +5380,7 @@ WITH agent_stats AS ( FROM workspace_agent_stats ) AS a WHERE a.rn = 1 ) -SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats CROSS JOIN latest_agent_stats +SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats, latest_agent_stats ` type GetDeploymentWorkspaceAgentStatsRow struct { diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 18e5db776dbd0..a542f67c08a82 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -69,4 +69,4 @@ WITH agent_stats AS ( FROM workspace_agent_stats ) AS a WHERE a.rn = 1 ) -SELECT * FROM agent_stats CROSS JOIN latest_agent_stats; +SELECT * FROM agent_stats, latest_agent_stats; From a1804a96db1000c532e3413ff74ca707d6c815eb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 6 Mar 2023 19:03:15 +0000 Subject: [PATCH 07/24] Fix continuous stats --- agent/agent.go | 9 +++++++++ agent/agent_test.go | 1 - cli/deployment/config.go | 2 +- go.mod | 2 +- go.sum | 2 ++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 725bbbd87c734..178d3f572cf40 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -17,6 +17,7 @@ import ( "os/exec" "os/user" "path/filepath" + "reflect" "runtime" "sort" "strconv" @@ -155,6 +156,7 @@ type agent struct { network *tailnet.Conn connStatsChan chan *agentsdk.Stats + latestStat atomic.Pointer[agentsdk.Stats] connCountVSCode atomic.Int64 connCountJetBrains atomic.Int64 @@ -1241,6 +1243,13 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { // Convert from microseconds to milliseconds. stats.ConnectionMedianLatencyMS /= 1000 + lastStat := a.latestStat.Load() + if lastStat != nil && reflect.DeepEqual(lastStat, stats) { + a.logger.Info(ctx, "skipping stat because nothing changed") + return + } + a.latestStat.Store(stats) + select { case a.connStatsChan <- stats: default: diff --git a/agent/agent_test.go b/agent/agent_test.go index e5ceb22182c97..1e7433933b80d 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -156,7 +156,6 @@ func TestAgent_Stats_Magic(t *testing.T) { require.Eventuallyf(t, func() bool { var ok bool s, ok = <-stats - fmt.Printf("WE GOT STATS %+v\n", s) return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && // Ensure that the connection didn't count as a "normal" SSH session. // This was a special one, so it should be labeled specially in the stats! diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 824a0286e2210..9a2b18efc050c 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -406,7 +406,7 @@ func newConfig() *codersdk.DeploymentConfig { Usage: "How frequently agent stats are recorded", Flag: "agent-stats-refresh-interval", Hidden: true, - Default: time.Minute, + Default: 30 * time.Second, }, AgentFallbackTroubleshootingURL: &codersdk.DeploymentConfigField[string]{ Name: "Agent Fallback Troubleshooting URL", diff --git a/go.mod b/go.mod index 48b6d5d4f2235..4355583d9c0d2 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ replace github.com/tcnksm/go-httpstat => github.com/kylecarbs/go-httpstat v0.0.0 // There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here: // https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main -replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230301203426-fb16ae7c5bba +replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230306185501-ae6dbba0a7ee // Switch to our fork that imports fixes from http://github.com/tailscale/ssh. // See: https://github.com/coder/coder/issues/3371 diff --git a/go.sum b/go.sum index 1021a4718afa4..1aa8060b44941 100644 --- a/go.sum +++ b/go.sum @@ -407,6 +407,8 @@ github.com/coder/tailscale v1.1.1-0.20230228220447-d27e5d3056e9 h1:bFcFXLUUi+cwg github.com/coder/tailscale v1.1.1-0.20230228220447-d27e5d3056e9/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA= github.com/coder/tailscale v1.1.1-0.20230301203426-fb16ae7c5bba h1:JOD5pqNtiz9lkSX74PY2BJOyNqsBmvGUjFGIuECtG+o= github.com/coder/tailscale v1.1.1-0.20230301203426-fb16ae7c5bba/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA= +github.com/coder/tailscale v1.1.1-0.20230306185501-ae6dbba0a7ee h1:0o3Q9u7+r7zNPaRCW/bLXuxPv8ukExtyig7RZfzcs8A= +github.com/coder/tailscale v1.1.1-0.20230306185501-ae6dbba0a7ee/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA= github.com/coder/terraform-provider-coder v0.6.14 h1:NsJ1mo0MN1x/VyNLYmsgPUYn2JgzdVNZBqnj9OSIDgY= github.com/coder/terraform-provider-coder v0.6.14/go.mod h1:UIfU3bYNeSzJJvHyJ30tEKjD6Z9utloI+HUM/7n94CY= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= From 93f013bf0f9b2c28f336e90168bf4a13aab56f3d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 6 Mar 2023 19:22:39 +0000 Subject: [PATCH 08/24] Increase period of stat refreshes --- coderd/metricscache/metricscache.go | 29 ++++++++++++++++------------- codersdk/deployment.go | 10 ++++++++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 57b2194425b75..06025e73a9619 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -26,8 +26,9 @@ import ( // take a few hundred milliseconds, which would ruin page load times and // database performance if in the hot path. type Cache struct { - database database.Store - log slog.Logger + database database.Store + log slog.Logger + intervals Intervals deploymentDAUResponses atomic.Pointer[codersdk.DeploymentDAUsResponse] templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse] @@ -54,23 +55,24 @@ func New(db database.Store, log slog.Logger, intervals Intervals) *Cache { ctx, cancel := context.WithCancel(context.Background()) c := &Cache{ - database: db, - log: log, - done: make(chan struct{}), - cancel: cancel, + database: db, + intervals: intervals, + log: log, + done: make(chan struct{}), + cancel: cancel, } go func() { var wg sync.WaitGroup defer close(c.done) wg.Add(1) go func() { - wg.Done() - c.run(ctx, intervals.TemplateDAUs, c.refreshTemplateDAUs) + defer wg.Done() + c.run(ctx, "template daus", intervals.TemplateDAUs, c.refreshTemplateDAUs) }() wg.Add(1) go func() { - wg.Done() - c.run(ctx, intervals.DeploymentStats, c.refreshDeploymentStats) + defer wg.Done() + c.run(ctx, "deployment stats", intervals.DeploymentStats, c.refreshDeploymentStats) }() wg.Wait() }() @@ -228,7 +230,8 @@ func (c *Cache) refreshDeploymentStats(ctx context.Context) error { } c.deploymentStatsResponse.Store(&codersdk.DeploymentStats{ AggregatedFrom: from, - UpdatedAt: database.Now(), + CollectedAt: database.Now(), + RefreshingAt: database.Now().Add(c.intervals.DeploymentStats), WorkspaceConnectionLatencyMS: codersdk.WorkspaceConnectionLatencyMS{ P50: deploymentStats.WorkspaceConnectionLatency50, P95: deploymentStats.WorkspaceConnectionLatency95, @@ -243,7 +246,7 @@ func (c *Cache) refreshDeploymentStats(ctx context.Context) error { return nil } -func (c *Cache) run(ctx context.Context, interval time.Duration, refresh func(context.Context) error) { +func (c *Cache) run(ctx context.Context, name string, interval time.Duration, refresh func(context.Context) error) { ticker := time.NewTicker(interval) defer ticker.Stop() @@ -260,7 +263,7 @@ func (c *Cache) run(ctx context.Context, interval time.Duration, refresh func(co } c.log.Debug( ctx, - "metrics refreshed", + name+" metrics refreshed", slog.F("took", time.Since(start)), slog.F("interval", interval), ) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 2a991140db32a..8f0cf4bc4671f 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -550,8 +550,14 @@ type WorkspaceConnectionLatencyMS struct { } type DeploymentStats struct { - AggregatedFrom time.Time `json:"aggregated_from"` - UpdatedAt time.Time `json:"updated_at"` + // AggregatedFrom is the time in which stats are aggregated from. + // This might be back in time a specific duration or interval. + AggregatedFrom time.Time `json:"aggregated_since"` + // CollectedAt is the time in which stats are collected at. + CollectedAt time.Time `json:"collected_at"` + // RefreshingAt is the time when the next batch of stats will + // be refreshed. + RefreshingAt time.Time `json:"refreshing_at"` WorkspacesByTransition map[WorkspaceTransition]int `json:"workspaces_by_transition"` WorkspaceConnectionLatencyMS WorkspaceConnectionLatencyMS `json:"workspace_connection_latency_ms"` From 50260c30cdfeb511ae1599e3aca08f3b9130b7ae Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 00:05:52 +0000 Subject: [PATCH 09/24] Add workspace counts to deployment stats --- coderd/apidoc/docs.go | 33 +- coderd/apidoc/swagger.json | 33 +- coderd/database/dbauthz/system.go | 4 + coderd/database/dbfake/databasefake.go | 44 +++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 84 +++++ coderd/database/queries/workspaces.sql | 62 ++++ coderd/metricscache/metricscache.go | 28 +- codersdk/deployment.go | 7 +- docs/api/general.md | 16 +- docs/api/schemas.md | 46 +-- site/src/api/typesGenerated.ts | 11 +- .../components/Dashboard/DashboardLayout.tsx | 19 +- .../DeploymentBanner/DeploymentBanner.tsx | 20 ++ .../DeploymentBannerView.stories.tsx | 17 + .../DeploymentBanner/DeploymentBannerView.tsx | 304 ++++++++++++++++++ site/src/components/Icons/RocketIcon.tsx | 7 + site/src/components/Icons/TerminalIcon.tsx | 7 + .../WorkspaceStatusBadge.tsx | 11 +- site/src/testHelpers/entities.ts | 22 ++ site/src/xServices/auth/authXService.ts | 7 + .../deploymentStats/deploymentStatsMachine.ts | 60 ++++ 22 files changed, 772 insertions(+), 71 deletions(-) create mode 100644 site/src/components/DeploymentBanner/DeploymentBanner.tsx create mode 100644 site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx create mode 100644 site/src/components/DeploymentBanner/DeploymentBannerView.tsx create mode 100644 site/src/components/Icons/RocketIcon.tsx create mode 100644 site/src/components/Icons/TerminalIcon.tsx create mode 100644 site/src/xServices/deploymentStats/deploymentStatsMachine.ts diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 87971463e325a..2bf9981a9522a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6624,9 +6624,30 @@ const docTemplate = `{ "codersdk.DeploymentStats": { "type": "object", "properties": { - "aggregated_from": { + "aggregated_since": { + "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", "type": "string" }, + "building_workspaces": { + "type": "integer" + }, + "collected_at": { + "description": "CollectedAt is the time in which stats are collected at.", + "type": "string" + }, + "failed_workspaces": { + "type": "integer" + }, + "pending_workspaces": { + "type": "integer" + }, + "refreshing_at": { + "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", + "type": "string" + }, + "running_workspaces": { + "type": "integer" + }, "session_count_jetbrains": { "type": "integer" }, @@ -6639,8 +6660,8 @@ const docTemplate = `{ "session_count_vscode": { "type": "integer" }, - "updated_at": { - "type": "string" + "stopped_workspaces": { + "type": "integer" }, "workspace_connection_latency_ms": { "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" @@ -6650,12 +6671,6 @@ const docTemplate = `{ }, "workspace_tx_bytes": { "type": "integer" - }, - "workspaces_by_transition": { - "type": "object", - "additionalProperties": { - "type": "integer" - } } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ea5943f670b03..34ac8f56b0895 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5928,9 +5928,30 @@ "codersdk.DeploymentStats": { "type": "object", "properties": { - "aggregated_from": { + "aggregated_since": { + "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", "type": "string" }, + "building_workspaces": { + "type": "integer" + }, + "collected_at": { + "description": "CollectedAt is the time in which stats are collected at.", + "type": "string" + }, + "failed_workspaces": { + "type": "integer" + }, + "pending_workspaces": { + "type": "integer" + }, + "refreshing_at": { + "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", + "type": "string" + }, + "running_workspaces": { + "type": "integer" + }, "session_count_jetbrains": { "type": "integer" }, @@ -5943,8 +5964,8 @@ "session_count_vscode": { "type": "integer" }, - "updated_at": { - "type": "string" + "stopped_workspaces": { + "type": "integer" }, "workspace_connection_latency_ms": { "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" @@ -5954,12 +5975,6 @@ }, "workspace_tx_bytes": { "type": "integer" - }, - "workspaces_by_transition": { - "type": "object", - "additionalProperties": { - "type": "integer" - } } } }, diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index 8a12442e5cae3..e83b0b0771f9d 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -205,6 +205,10 @@ func (q *querier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdA return q.db.GetDeploymentWorkspaceAgentStats(ctx, createdAfter) } +func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.GetDeploymentWorkspaceStatsRow, error) { + return q.db.GetDeploymentWorkspaceStats(ctx) +} + func (q *querier) GetParameterSchemasCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.ParameterSchema, error) { return q.db.GetParameterSchemasCreatedAfter(ctx, createdAt) } diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 99f1668987545..1b849ee8648f7 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -3547,6 +3547,50 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } +func (q *fakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database.GetDeploymentWorkspaceStatsRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + stat := database.GetDeploymentWorkspaceStatsRow{} + for _, workspace := range q.workspaces { + build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return stat, err + } + job, err := q.getProvisionerJobByIDNoLock(ctx, build.JobID) + if err != nil { + return stat, err + } + if !job.StartedAt.Valid { + stat.PendingWorkspaces++ + continue + } + if job.StartedAt.Valid && + !job.CanceledAt.Valid && + time.Since(job.UpdatedAt) <= 30*time.Second && + !job.CompletedAt.Valid { + stat.BuildingWorkspaces++ + continue + } + if job.CompletedAt.Valid && + !job.CanceledAt.Valid && + !job.Error.Valid { + if build.Transition == database.WorkspaceTransitionStart { + stat.RunningWorkspaces++ + } + if build.Transition == database.WorkspaceTransitionStop { + stat.StoppedWorkspaces++ + } + continue + } + if job.CanceledAt.Valid || job.Error.Valid { + stat.FailedWorkspaces++ + continue + } + } + return stat, nil +} + func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceBuild{}, err diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b989fd44d0471..be60d37f26319 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -44,6 +44,7 @@ type sqlcQuerier interface { GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error) GetDeploymentID(ctx context.Context) (string, error) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) + GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) // This will never count deleted users. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 10eca8b130347..9664286d05642 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6691,6 +6691,90 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one +WITH workspaces_with_jobs AS ( + SELECT + latest_build.transition, latest_build.provisioner_job_id, latest_build.started_at, latest_build.updated_at, latest_build.canceled_at, latest_build.completed_at, latest_build.error FROM workspaces + LEFT JOIN LATERAL ( + SELECT + workspace_builds.transition, + provisioner_jobs.id AS provisioner_job_id, + provisioner_jobs.started_at, + provisioner_jobs.updated_at, + provisioner_jobs.canceled_at, + provisioner_jobs.completed_at, + provisioner_jobs.error + FROM + workspace_builds + LEFT JOIN + provisioner_jobs + ON + provisioner_jobs.id = workspace_builds.job_id + WHERE + workspace_builds.workspace_id = workspaces.id + ORDER BY + build_number DESC + LIMIT + 1 + ) latest_build ON TRUE +), pending_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + started_at IS NULL +), building_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + started_at IS NOT NULL AND + canceled_at IS NULL AND + updated_at - INTERVAL '30 seconds' < NOW() AND + completed_at IS NULL +), running_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + completed_at IS NOT NULL AND + canceled_at IS NULL AND + error IS NULL AND + transition = 'start'::workspace_transition +), failed_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + (canceled_at IS NOT NULL AND + error IS NOT NULL) OR + (completed_at IS NOT NULL AND + error IS NOT NULL) +), stopped_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + completed_at IS NOT NULL AND + canceled_at IS NULL AND + error IS NULL AND + transition = 'stop'::workspace_transition +) +SELECT + pending_workspaces.count AS pending_workspaces, + building_workspaces.count AS building_workspaces, + running_workspaces.count AS running_workspaces, + failed_workspaces.count AS failed_workspaces, + stopped_workspaces.count AS stopped_workspaces +FROM pending_workspaces, building_workspaces, running_workspaces, failed_workspaces, stopped_workspaces +` + +type GetDeploymentWorkspaceStatsRow struct { + PendingWorkspaces int64 `db:"pending_workspaces" json:"pending_workspaces"` + BuildingWorkspaces int64 `db:"building_workspaces" json:"building_workspaces"` + RunningWorkspaces int64 `db:"running_workspaces" json:"running_workspaces"` + FailedWorkspaces int64 `db:"failed_workspaces" json:"failed_workspaces"` + StoppedWorkspaces int64 `db:"stopped_workspaces" json:"stopped_workspaces"` +} + +func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) { + row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceStats) + var i GetDeploymentWorkspaceStatsRow + err := row.Scan( + &i.PendingWorkspaces, + &i.BuildingWorkspaces, + &i.RunningWorkspaces, + &i.FailedWorkspaces, + &i.StoppedWorkspaces, + ) + 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 diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index def4436bed94c..6b06972fd1980 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -316,3 +316,65 @@ SET last_used_at = $2 WHERE id = $1; + +-- name: GetDeploymentWorkspaceStats :one +WITH workspaces_with_jobs AS ( + SELECT + latest_build.* FROM workspaces + LEFT JOIN LATERAL ( + SELECT + workspace_builds.transition, + provisioner_jobs.id AS provisioner_job_id, + provisioner_jobs.started_at, + provisioner_jobs.updated_at, + provisioner_jobs.canceled_at, + provisioner_jobs.completed_at, + provisioner_jobs.error + FROM + workspace_builds + LEFT JOIN + provisioner_jobs + ON + provisioner_jobs.id = workspace_builds.job_id + WHERE + workspace_builds.workspace_id = workspaces.id + ORDER BY + build_number DESC + LIMIT + 1 + ) latest_build ON TRUE +), pending_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + started_at IS NULL +), building_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + started_at IS NOT NULL AND + canceled_at IS NULL AND + updated_at - INTERVAL '30 seconds' < NOW() AND + completed_at IS NULL +), running_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + completed_at IS NOT NULL AND + canceled_at IS NULL AND + error IS NULL AND + transition = 'start'::workspace_transition +), failed_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + (canceled_at IS NOT NULL AND + error IS NOT NULL) OR + (completed_at IS NOT NULL AND + error IS NOT NULL) +), stopped_workspaces AS ( + SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE + completed_at IS NOT NULL AND + canceled_at IS NULL AND + error IS NULL AND + transition = 'stop'::workspace_transition +) +SELECT + pending_workspaces.count AS pending_workspaces, + building_workspaces.count AS building_workspaces, + running_workspaces.count AS running_workspaces, + failed_workspaces.count AS failed_workspaces, + stopped_workspaces.count AS stopped_workspaces +FROM pending_workspaces, building_workspaces, running_workspaces, failed_workspaces, stopped_workspaces; diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 06025e73a9619..52b769087335e 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -224,7 +224,11 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { func (c *Cache) refreshDeploymentStats(ctx context.Context) error { from := database.Now().Add(-15 * time.Minute) - deploymentStats, err := c.database.GetDeploymentWorkspaceAgentStats(ctx, from) + agentStats, err := c.database.GetDeploymentWorkspaceAgentStats(ctx, from) + if err != nil { + return err + } + workspaceStats, err := c.database.GetDeploymentWorkspaceStats(ctx) if err != nil { return err } @@ -233,15 +237,21 @@ func (c *Cache) refreshDeploymentStats(ctx context.Context) error { CollectedAt: database.Now(), RefreshingAt: database.Now().Add(c.intervals.DeploymentStats), WorkspaceConnectionLatencyMS: codersdk.WorkspaceConnectionLatencyMS{ - P50: deploymentStats.WorkspaceConnectionLatency50, - P95: deploymentStats.WorkspaceConnectionLatency95, + P50: agentStats.WorkspaceConnectionLatency50, + P95: agentStats.WorkspaceConnectionLatency95, }, - SessionCountVSCode: deploymentStats.SessionCountVSCode, - SessionCountSSH: deploymentStats.SessionCountSSH, - SessionCountJetBrains: deploymentStats.SessionCountJetBrains, - SessionCountReconnectingPTY: deploymentStats.SessionCountReconnectingPTY, - WorkspaceRxBytes: deploymentStats.WorkspaceRxBytes, - WorkspaceTxBytes: deploymentStats.WorkspaceTxBytes, + SessionCountVSCode: agentStats.SessionCountVSCode, + SessionCountSSH: agentStats.SessionCountSSH, + SessionCountJetBrains: agentStats.SessionCountJetBrains, + SessionCountReconnectingPTY: agentStats.SessionCountReconnectingPTY, + WorkspaceRxBytes: agentStats.WorkspaceRxBytes, + WorkspaceTxBytes: agentStats.WorkspaceTxBytes, + + PendingWorkspaces: workspaceStats.PendingWorkspaces, + BuildingWorkspaces: workspaceStats.BuildingWorkspaces, + RunningWorkspaces: workspaceStats.RunningWorkspaces, + FailedWorkspaces: workspaceStats.FailedWorkspaces, + StoppedWorkspaces: workspaceStats.StoppedWorkspaces, }) return nil } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 8f0cf4bc4671f..428074b26239c 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -559,7 +559,12 @@ type DeploymentStats struct { // be refreshed. RefreshingAt time.Time `json:"refreshing_at"` - WorkspacesByTransition map[WorkspaceTransition]int `json:"workspaces_by_transition"` + PendingWorkspaces int64 `json:"pending_workspaces"` + BuildingWorkspaces int64 `json:"building_workspaces"` + RunningWorkspaces int64 `json:"running_workspaces"` + FailedWorkspaces int64 `json:"failed_workspaces"` + StoppedWorkspaces int64 `json:"stopped_workspaces"` + WorkspaceConnectionLatencyMS WorkspaceConnectionLatencyMS `json:"workspace_connection_latency_ms"` SessionCountVSCode int64 `json:"session_count_vscode"` diff --git a/docs/api/general.md b/docs/api/general.md index 881ae443e1c9d..56c1bb606da92 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -1186,22 +1186,24 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \ ```json { - "aggregated_from": "string", + "aggregated_since": "string", + "building_workspaces": 0, + "collected_at": "string", + "failed_workspaces": 0, + "pending_workspaces": 0, + "refreshing_at": "string", + "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, "session_count_ssh": 0, "session_count_vscode": 0, - "updated_at": "string", + "stopped_workspaces": 0, "workspace_connection_latency_ms": { "p50": 0, "p95": 0 }, "workspace_rx_bytes": 0, - "workspace_tx_bytes": 0, - "workspaces_by_transition": { - "property1": 0, - "property2": 0 - } + "workspace_tx_bytes": 0 } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 141457c971596..93941f9075849 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2921,40 +2921,46 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { - "aggregated_from": "string", + "aggregated_since": "string", + "building_workspaces": 0, + "collected_at": "string", + "failed_workspaces": 0, + "pending_workspaces": 0, + "refreshing_at": "string", + "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, "session_count_ssh": 0, "session_count_vscode": 0, - "updated_at": "string", + "stopped_workspaces": 0, "workspace_connection_latency_ms": { "p50": 0, "p95": 0 }, "workspace_rx_bytes": 0, - "workspace_tx_bytes": 0, - "workspaces_by_transition": { - "property1": 0, - "property2": 0 - } + "workspace_tx_bytes": 0 } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | -| `aggregated_from` | string | false | | | -| `session_count_jetbrains` | integer | false | | | -| `session_count_reconnecting_pty` | integer | false | | | -| `session_count_ssh` | integer | false | | | -| `session_count_vscode` | integer | false | | | -| `updated_at` | string | false | | | -| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | -| `workspace_rx_bytes` | integer | false | | | -| `workspace_tx_bytes` | integer | false | | | -| `workspaces_by_transition` | object | false | | | -| ยป `[any property]` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | +| `aggregated_since` | string | false | | Aggregated since is the time in which stats are aggregated from. This might be back in time a specific duration or interval. | +| `building_workspaces` | integer | false | | | +| `collected_at` | string | false | | Collected at is the time in which stats are collected at. | +| `failed_workspaces` | integer | false | | | +| `pending_workspaces` | integer | false | | | +| `refreshing_at` | string | false | | Refreshing at is the time when the next batch of stats will be refreshed. | +| `running_workspaces` | integer | false | | | +| `session_count_jetbrains` | integer | false | | | +| `session_count_reconnecting_pty` | integer | false | | | +| `session_count_ssh` | integer | false | | | +| `session_count_vscode` | integer | false | | | +| `stopped_workspaces` | integer | false | | | +| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | +| `workspace_rx_bytes` | integer | false | | | +| `workspace_tx_bytes` | integer | false | | | ## codersdk.Entitlement diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b4b723550abac..62223b18a2ea5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -363,9 +363,14 @@ export interface DeploymentDAUsResponse { // From codersdk/deployment.go export interface DeploymentStats { - readonly aggregated_from: string - readonly updated_at: string - readonly workspaces_by_transition: Record + readonly aggregated_since: string + readonly collected_at: string + readonly refreshing_at: string + readonly pending_workspaces: number + readonly building_workspaces: number + readonly running_workspaces: number + readonly failed_workspaces: number + readonly stopped_workspaces: number readonly workspace_connection_latency_ms: WorkspaceConnectionLatencyMS readonly session_count_vscode: number readonly session_count_ssh: number diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx index 1dfd2a15c0e4c..843cf77e041f5 100644 --- a/site/src/components/Dashboard/DashboardLayout.tsx +++ b/site/src/components/Dashboard/DashboardLayout.tsx @@ -1,18 +1,19 @@ import { makeStyles } from "@material-ui/core/styles" import { useMachine } from "@xstate/react" +import { UpdateCheckResponse } from "api/typesGenerated" +import { DeploymentBanner } from "components/DeploymentBanner/DeploymentBanner" +import { LicenseBanner } from "components/LicenseBanner/LicenseBanner" import { Loader } from "components/Loader/Loader" -import { FC, Suspense } from "react" -import { Navbar } from "../Navbar/Navbar" -import { UpdateCheckBanner } from "components/UpdateCheckBanner/UpdateCheckBanner" import { Margins } from "components/Margins/Margins" -import { Outlet } from "react-router-dom" -import { LicenseBanner } from "components/LicenseBanner/LicenseBanner" import { ServiceBanner } from "components/ServiceBanner/ServiceBanner" -import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService" +import { UpdateCheckBanner } from "components/UpdateCheckBanner/UpdateCheckBanner" import { usePermissions } from "hooks/usePermissions" -import { UpdateCheckResponse } from "api/typesGenerated" -import { DashboardProvider } from "./DashboardProvider" +import { FC, Suspense } from "react" +import { Outlet } from "react-router-dom" import { dashboardContentBottomPadding } from "theme/constants" +import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService" +import { Navbar } from "../Navbar/Navbar" +import { DashboardProvider } from "./DashboardProvider" export const DashboardLayout: FC = () => { const styles = useStyles() @@ -51,6 +52,8 @@ export const DashboardLayout: FC = () => { + + ) diff --git a/site/src/components/DeploymentBanner/DeploymentBanner.tsx b/site/src/components/DeploymentBanner/DeploymentBanner.tsx new file mode 100644 index 0000000000000..2c1bcd7ec5074 --- /dev/null +++ b/site/src/components/DeploymentBanner/DeploymentBanner.tsx @@ -0,0 +1,20 @@ +import { useMachine } from "@xstate/react" +import { usePermissions } from "hooks/usePermissions" +import { DeploymentBannerView } from "./DeploymentBannerView" +import { deploymentStatsMachine } from "../../xServices/deploymentStats/deploymentStatsMachine" + +export const DeploymentBanner: React.FC = () => { + const permissions = usePermissions() + const [state, sendEvent] = useMachine(deploymentStatsMachine) + + if (!permissions.viewDeploymentConfig || !state.context.deploymentStats) { + return null + } + + return ( + sendEvent("RELOAD")} + /> + ) +} diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx new file mode 100644 index 0000000000000..987551ea47e1f --- /dev/null +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx @@ -0,0 +1,17 @@ +import { Story } from "@storybook/react" +import { MockDeploymentStats } from "testHelpers/entities" +import { DeploymentBannerView, DeploymentBannerViewProps } from "./DeploymentBannerView" + +export default { + title: "components/DeploymentBannerView", + component: DeploymentBannerView, +} + +const Template: Story = (args) => ( + +) + +export const Preview = Template.bind({}) +Preview.args = { + stats: MockDeploymentStats, +} diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx new file mode 100644 index 0000000000000..5ce60a8b66413 --- /dev/null +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -0,0 +1,304 @@ +import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated" +import { FC, useMemo, useEffect, useState } from "react" +import prettyBytes from "pretty-bytes" +import { getStatus } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" +import BuildingIcon from "@material-ui/icons/Build" +import { makeStyles } from "@material-ui/core/styles" +import { RocketIcon } from "components/Icons/RocketIcon" +import { MONOSPACE_FONT_FAMILY } from "theme/constants" +import Tooltip from "@material-ui/core/Tooltip" +import { Link as RouterLink } from "react-router-dom" +import Link from "@material-ui/core/Link" +import InfoIcon from "@material-ui/icons/InfoOutlined" +import { VSCodeIcon } from "components/Icons/VSCodeIcon" +import DownloadIcon from "@material-ui/icons/CloudDownload" +import UploadIcon from "@material-ui/icons/CloudUpload" +import LatencyIcon from "@material-ui/icons/SettingsEthernet" +import WebTerminalIcon from "@material-ui/icons/WebAsset" +import { TerminalIcon } from "components/Icons/TerminalIcon" +import dayjs from "dayjs" +import CollectedIcon from "@material-ui/icons/Compare" +import RefreshIcon from "@material-ui/icons/Refresh" + +export interface DeploymentBannerViewProps { + fetchStats?: () => void + stats?: DeploymentStats +} + +export const DeploymentBannerView: FC = ({ + stats, + fetchStats, +}) => { + const styles = useStyles() + const aggregatedMinutes = useMemo(() => { + if (!stats) { + return + } + return dayjs(stats.collected_at).diff(stats.aggregated_since, "minutes") + }, [stats]) + const displayLatency = stats?.workspace_connection_latency_ms.P50 || -1 + const [timeUntilRefresh, setTimeUntilRefresh] = useState(0) + useEffect(() => { + if (!stats || !fetchStats) { + return + } + + let timeUntilRefresh = dayjs(stats.refreshing_at).diff( + stats.collected_at, + "seconds", + ) + setTimeUntilRefresh(timeUntilRefresh) + let canceled = false + const loop = () => { + if (canceled) { + return + } + setTimeUntilRefresh(timeUntilRefresh--) + if (timeUntilRefresh > 0) { + return setTimeout(loop, 1000) + } + fetchStats() + } + const timeout = setTimeout(loop, 1000) + return () => { + canceled = true + clearTimeout(timeout) + } + }, [fetchStats, stats]) + const lastAggregated = useMemo(() => { + if (!stats) { + return + } + if (!fetchStats) { + // Storybook! + return "just now" + } + return dayjs().to(dayjs(stats.collected_at)) + // eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update! + }, [timeUntilRefresh, stats]) + + return ( +
+ +
+ +
+
+
+
Workspaces
+
+ + + + + + + + + +
+
+
+ +
+ Transmission + +
+
+ +
+ +
+ + {stats ? prettyBytes(stats.workspace_rx_bytes) : "-"} +
+
+ + +
+ + {stats ? prettyBytes(stats.workspace_tx_bytes) : "-"} +
+
+ + +
+ + {displayLatency > 0 ? displayLatency?.toFixed(2) + "ms" : "-"} +
+
+
+
+
+
Active Connections
+ +
+ +
+ + {typeof stats?.session_count_vscode === "undefined" + ? "-" + : stats?.session_count_vscode} +
+
+ + +
+ + {typeof stats?.session_count_ssh === "undefined" + ? "-" + : stats?.session_count_ssh} +
+
+ + +
+ + {typeof stats?.session_count_reconnecting_pty === "undefined" + ? "-" + : stats?.session_count_reconnecting_pty} +
+
+
+
+
+ +
+ + {lastAggregated} +
+
+ + +
+ + {timeUntilRefresh}s +
+
+
+
+ ) +} + +const ValueSeparator: FC = () => { + const styles = useStyles() + return
/
+} + +const WorkspaceBuildValue: FC<{ + status: WorkspaceStatus + count?: number +}> = ({ status, count }) => { + const styles = useStyles() + const displayStatus = getStatus(status) + let statusText = displayStatus.text + let icon = displayStatus.icon + if (status === "starting") { + icon = + statusText = "Building" + } + + return ( + + +
+ {icon} + {typeof count === "undefined" ? "-" : count} +
+ +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + rocket: { + display: "flex", + alignItems: "center", + + "& svg": { + width: 16, + height: 16, + }, + }, + container: { + position: "sticky", + bottom: 0, + zIndex: 1, + padding: theme.spacing(1, 2), + backgroundColor: theme.palette.background.paper, + display: "flex", + alignItems: "center", + fontFamily: MONOSPACE_FONT_FAMILY, + fontSize: 12, + gap: theme.spacing(4), + borderTop: `1px solid ${theme.palette.divider}`, + }, + group: { + display: "flex", + alignItems: "center", + }, + category: { + marginRight: theme.spacing(2), + color: theme.palette.text.hint, + + "& svg": { + width: 12, + height: 12, + marginBottom: 2, + }, + }, + values: { + display: "flex", + gap: theme.spacing(1), + color: theme.palette.text.secondary, + }, + valueSeparator: { + color: theme.palette.text.disabled, + }, + value: { + display: "flex", + alignItems: "center", + gap: theme.spacing(0.5), + + "& svg": { + width: 12, + height: 12, + }, + }, + iconStripColor: { + "& *": { + fill: "currentColor", + }, + }, + refresh: { + color: theme.palette.text.hint, + marginLeft: "auto", + display: "flex", + alignItems: "center", + gap: theme.spacing(2), + }, +})) diff --git a/site/src/components/Icons/RocketIcon.tsx b/site/src/components/Icons/RocketIcon.tsx new file mode 100644 index 0000000000000..0973866ea76a1 --- /dev/null +++ b/site/src/components/Icons/RocketIcon.tsx @@ -0,0 +1,7 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon" + +export const RocketIcon: typeof SvgIcon = (props: SvgIconProps) => ( + + + +) diff --git a/site/src/components/Icons/TerminalIcon.tsx b/site/src/components/Icons/TerminalIcon.tsx new file mode 100644 index 0000000000000..24a0b98a5fa37 --- /dev/null +++ b/site/src/components/Icons/TerminalIcon.tsx @@ -0,0 +1,7 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon" + +export const TerminalIcon: typeof SvgIcon = (props: SvgIconProps) => ( + + + +) diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 1df7f9a9a340c..0a23ac7409bfa 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -1,7 +1,8 @@ import CircularProgress from "@material-ui/core/CircularProgress" import ErrorIcon from "@material-ui/icons/ErrorOutline" -import StopIcon from "@material-ui/icons/PauseOutlined" +import StopIcon from "@material-ui/icons/StopOutlined" import PlayIcon from "@material-ui/icons/PlayArrowOutlined" +import QueuedIcon from "@material-ui/icons/HourglassEmpty" import { WorkspaceBuild } from "api/typesGenerated" import { Pill } from "components/Pill/Pill" import i18next from "i18next" @@ -13,7 +14,7 @@ const LoadingIcon: FC = () => { } export const getStatus = ( - build: WorkspaceBuild, + buildStatus: WorkspaceBuild["status"], ): { type?: PaletteIndex text: string @@ -21,7 +22,7 @@ export const getStatus = ( } => { const { t } = i18next - switch (build.status) { + switch (buildStatus) { case undefined: return { text: t("workspaceStatus.loading", { ns: "common" }), @@ -85,7 +86,7 @@ export const getStatus = ( return { type: "info", text: t("workspaceStatus.pending", { ns: "common" }), - icon: , + icon: , } } } @@ -98,6 +99,6 @@ export type WorkspaceStatusBadgeProps = { export const WorkspaceStatusBadge: FC< PropsWithChildren > = ({ build, className }) => { - const { text, icon, type } = getStatus(build) + const { text, icon, type } = getStatus(build.status) return } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3ae63bc9c3cff..db55edd7cf6b0 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1411,6 +1411,7 @@ export const MockPermissions: Permissions = { viewAuditLog: true, viewDeploymentConfig: true, viewUpdateCheck: true, + viewDeploymentStats: true, } export const MockAppearance: TypesGen.AppearanceConfig = { @@ -1466,3 +1467,24 @@ export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = { authenticate_url: "https://example.com/gitauth/github", authenticated: false, } + +export const MockDeploymentStats: TypesGen.DeploymentStats = { + aggregated_since: "2023-03-06T19:08:55.211625Z", + collected_at: "2023-03-06T19:12:55.211625Z", + refreshing_at: "2023-03-06T19:20:55.211625Z", + session_count_jetbrains: 5, + session_count_reconnecting_pty: 15, + session_count_ssh: 32, + session_count_vscode: 128, + workspace_connection_latency_ms: { + P50: 32.56, + P95: 15.23, + }, + workspace_rx_bytes: 15613513253, + workspace_tx_bytes: 36113513253, + building_workspaces: 15, + failed_workspaces: 12, + pending_workspaces: 5, + running_workspaces: 32, + stopped_workspaces: 16, +} diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index de3d36745949c..a932fbb8e3648 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -17,6 +17,7 @@ export const checks = { viewDeploymentConfig: "viewDeploymentConfig", createGroup: "createGroup", viewUpdateCheck: "viewUpdateCheck", + viewDeploymentStats: "viewDeploymentStats", } as const export const permissionsToCheck = { @@ -74,6 +75,12 @@ export const permissionsToCheck = { }, action: "read", }, + [checks.viewDeploymentStats]: { + object: { + resource_type: "deployment_stats", + }, + action: "read", + }, } as const export type Permissions = Record diff --git a/site/src/xServices/deploymentStats/deploymentStatsMachine.ts b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts new file mode 100644 index 0000000000000..9affa96771bb1 --- /dev/null +++ b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts @@ -0,0 +1,60 @@ +import { getDeploymentStats } from "api/api" +import { DeploymentStats } from "api/typesGenerated" +import { assign, createMachine } from "xstate" + +export const deploymentStatsMachine = createMachine( + { + id: "deploymentStatsMachine", + predictableActionArguments: true, + + schema: { + context: {} as { + deploymentStats?: DeploymentStats + getDeploymentStatsError?: unknown + }, + events: {} as { type: "RELOAD" }, + services: {} as { + getDeploymentStats: { + data: DeploymentStats + } + }, + + }, + tsTypes: {} as import("./deploymentStatsMachine.typegen").Typegen0, + initial: "stats", + states: { + stats: { + invoke: { + src: "getDeploymentStats", + onDone: { + target: "idle", + actions: ["assignDeploymentStats"], + }, + onError: { + target: "idle", + actions: ["assignDeploymentStatsError"], + }, + }, + tags: "loading", + }, + idle: { + on: { + RELOAD: "stats", + }, + }, + }, + }, + { + services: { + getDeploymentStats: getDeploymentStats, + }, + actions: { + assignDeploymentStats: assign({ + deploymentStats: (_, { data }) => data, + }), + assignDeploymentStatsError: assign({ + getDeploymentStatsError: (_, { data }) => data, + }), + }, + }, +) From d1bae998d6558cd7598d9377abc90917be3aadcb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 01:23:25 +0000 Subject: [PATCH 10/24] fmt --- .../DeploymentBanner/DeploymentBannerView.stories.tsx | 5 ++++- .../components/DeploymentBanner/DeploymentBannerView.tsx | 6 +++--- site/src/xServices/auth/authXService.ts | 2 +- .../src/xServices/deploymentStats/deploymentStatsMachine.ts | 1 - 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx index 987551ea47e1f..dd54d531a589b 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.stories.tsx @@ -1,6 +1,9 @@ import { Story } from "@storybook/react" import { MockDeploymentStats } from "testHelpers/entities" -import { DeploymentBannerView, DeploymentBannerViewProps } from "./DeploymentBannerView" +import { + DeploymentBannerView, + DeploymentBannerViewProps, +} from "./DeploymentBannerView" export default { title: "components/DeploymentBannerView", diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index 5ce60a8b66413..5f8e53a501a32 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -74,7 +74,7 @@ export const DeploymentBannerView: FC = ({ return "just now" } return dayjs().to(dayjs(stats.collected_at)) - // eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update! + // eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update! }, [timeUntilRefresh, stats]) return ( @@ -145,7 +145,7 @@ export const DeploymentBannerView: FC = ({ >
- {displayLatency > 0 ? displayLatency?.toFixed(2) + "ms" : "-"} + {displayLatency > 0 ? displayLatency?.toFixed(2) + " ms" : "-"}
@@ -183,7 +183,7 @@ export const DeploymentBannerView: FC = ({
- +
{lastAggregated} diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index a932fbb8e3648..d6a96c8ac9240 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -79,7 +79,7 @@ export const permissionsToCheck = { object: { resource_type: "deployment_stats", }, - action: "read", + action: "read", }, } as const diff --git a/site/src/xServices/deploymentStats/deploymentStatsMachine.ts b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts index 9affa96771bb1..a68447b54b45a 100644 --- a/site/src/xServices/deploymentStats/deploymentStatsMachine.ts +++ b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts @@ -18,7 +18,6 @@ export const deploymentStatsMachine = createMachine( data: DeploymentStats } }, - }, tsTypes: {} as import("./deploymentStatsMachine.typegen").Typegen0, initial: "stats", From 9fe9d4cf20b325b4a36cedc9eac97d0c26b4d675 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 01:27:35 +0000 Subject: [PATCH 11/24] Add a slight bit of responsiveness --- .../DeploymentBanner/DeploymentBannerView.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index 5f8e53a501a32..cafe179d17ee0 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -243,6 +243,10 @@ const useStyles = makeStyles((theme) => ({ width: 16, height: 16, }, + + [theme.breakpoints.down("md")]: { + display: "none", + }, }, container: { position: "sticky", @@ -256,6 +260,12 @@ const useStyles = makeStyles((theme) => ({ fontSize: 12, gap: theme.spacing(4), borderTop: `1px solid ${theme.palette.divider}`, + + [theme.breakpoints.down("md")]: { + flexDirection: "column", + gap: theme.spacing(1), + alignItems: "left", + }, }, group: { display: "flex", From 00ebe2e85b90b0a65078f6f9f92ecca98895629c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 01:32:43 +0000 Subject: [PATCH 12/24] Fix template version editor overflow --- .../DeploymentBanner/DeploymentBannerView.tsx | 3 +++ .../TemplateVersionEditor/TemplateVersionEditor.tsx | 10 +++++++++- .../TemplateVersionEditorPage.tsx | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index cafe179d17ee0..acf797bf70db1 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -20,6 +20,8 @@ import dayjs from "dayjs" import CollectedIcon from "@material-ui/icons/Compare" import RefreshIcon from "@material-ui/icons/Refresh" +export const bannerHeight = 36 + export interface DeploymentBannerViewProps { fetchStats?: () => void stats?: DeploymentStats @@ -250,6 +252,7 @@ const useStyles = makeStyles((theme) => ({ }, container: { position: "sticky", + height: bannerHeight, bottom: 0, zIndex: 1, padding: theme.spacing(1, 2), diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 0ef80162ae375..228a9064a31aa 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -13,6 +13,7 @@ import { } from "api/typesGenerated" import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" +import { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" import { FC, useCallback, useEffect, useRef, useState } from "react" @@ -44,6 +45,7 @@ export interface TemplateVersionEditorProps { defaultFileTree: FileTree buildLogs?: ProvisionerJobLog[] resources?: WorkspaceResource[] + deploymentBannerVisible?: boolean disablePreview: boolean disableUpdate: boolean onPreview: (files: FileTree) => void @@ -68,6 +70,7 @@ export const TemplateVersionEditor: FC = ({ disablePreview, disableUpdate, template, + deploymentBannerVisible, templateVersion, defaultFileTree, onPreview, @@ -146,6 +149,7 @@ export const TemplateVersionEditor: FC = ({ const styles = useStyles({ templateVersionSucceeded, showBuildLogs, + deploymentBannerVisible, }) return ( @@ -385,10 +389,14 @@ const useStyles = makeStyles< { templateVersionSucceeded: boolean showBuildLogs: boolean + deploymentBannerVisible: boolean } >((theme) => ({ root: { - height: `calc(100vh - ${navHeight}px)`, + height: (props) => + `calc(100vh - ${ + navHeight + (props.deploymentBannerVisible ? bannerHeight : 0) + }px)`, background: theme.palette.background.default, flex: 1, display: "flex", diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index c6702a3ebf2b2..d5dcfd71abfa3 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -1,6 +1,7 @@ import { useMachine } from "@xstate/react" import { TemplateVersionEditor } from "components/TemplateVersionEditor/TemplateVersionEditor" import { useOrganizationId } from "hooks/useOrganizationId" +import { usePermissions } from "hooks/usePermissions" import { FC } from "react" import { Helmet } from "react-helmet-async" import { useParams } from "react-router-dom" @@ -19,6 +20,7 @@ export const TemplateVersionEditorPage: FC = () => { const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { context: { orgId }, }) + const permissions = usePermissions() const { isSuccess, data } = useTemplateVersionData( { orgId, @@ -41,6 +43,7 @@ export const TemplateVersionEditorPage: FC = () => { {isSuccess && ( { From cd765332311ae71639ac13ad25675a39cd26a1db Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 01:40:14 +0000 Subject: [PATCH 13/24] Add refresh button --- .../DeploymentBanner/DeploymentBannerView.tsx | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index acf797bf70db1..07afd6b8a20dd 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -19,6 +19,7 @@ import { TerminalIcon } from "components/Icons/TerminalIcon" import dayjs from "dayjs" import CollectedIcon from "@material-ui/icons/Compare" import RefreshIcon from "@material-ui/icons/Refresh" +import Button from "@material-ui/core/Button" export const bannerHeight = 36 @@ -192,11 +193,20 @@ export const DeploymentBannerView: FC = ({
- -
+ +
+
@@ -314,4 +324,18 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: theme.spacing(2), }, + refreshButton: { + margin: 0, + padding: "0px 8px", + height: "unset", + minHeight: "unset", + fontSize: "unset", + color: "unset", + border: 0, + minWidth: "unset", + + "& svg": { + marginRight: theme.spacing(0.5), + }, + }, })) From 506740bcf35b0352ef12e658f5b31cfdbe48874e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 01:40:41 +0000 Subject: [PATCH 14/24] Fix font family on button --- site/src/components/DeploymentBanner/DeploymentBannerView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index 07afd6b8a20dd..c9202e58811bf 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -333,6 +333,7 @@ const useStyles = makeStyles((theme) => ({ color: "unset", border: 0, minWidth: "unset", + fontFamily: "inherit", "& svg": { marginRight: theme.spacing(0.5), From 9f00ac500e5a5617f205c16ec9917be062136d50 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 03:21:26 +0000 Subject: [PATCH 15/24] Fix latest stat being reported --- agent/agent.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/agent.go b/agent/agent.go index 1ff020d3584c7..f59fa1027ad4e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1276,6 +1276,9 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { select { case a.connStatsChan <- stats: + // Only store the latest stat when it's successfully sent! + // Otherwise, it should be sent again on the next iteration. + a.latestStat.Store(stats) default: a.logger.Warn(ctx, "network stat dropped") } From 1af9f64c8bffbb9b0c3c73b4a7bd77f0b9ea9505 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 04:11:11 +0000 Subject: [PATCH 16/24] Revert agent conn stats --- agent/agent.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 24276b1099f0b..f59fa1027ad4e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -123,9 +123,7 @@ func New(options Options) io.Closer { tempDir: options.TempDir, lifecycleUpdate: make(chan struct{}, 1), lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1), - // TODO: This is a temporary hack to make tests not flake. - // @kylecarbs has a better solution in here: https://github.com/coder/coder/pull/6469 - connStatsChan: make(chan *agentsdk.Stats, 8), + connStatsChan: make(chan *agentsdk.Stats, 1), } a.init(ctx) return a From 8ad39d6195a46e365b17fe6e69b38ca7ecbbbb8c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 16:14:29 +0000 Subject: [PATCH 17/24] Fix linting error --- agent/agent_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index cb24559e90ce9..6f856ffdaca48 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -116,7 +116,8 @@ func TestAgent_Stats_Magic(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - conn, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) + //nolint:dogsled + conn, _, _, _, _ := setupAgent(t, agentsdk.Metadata{}, 0) sshClient, err := conn.SSHClient(ctx) require.NoError(t, err) defer sshClient.Close() From 0f06b23ce3a7f2d7e61e7bf90d562547358c8fa7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 16:56:51 +0000 Subject: [PATCH 18/24] Fix tests --- agent/agent_test.go | 8 +++++++- codersdk/deployment.go | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index 6f856ffdaca48..9e2ff33d2d5c2 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -68,7 +68,10 @@ func TestAgent_Stats_SSH(t *testing.T) { session, err := sshClient.NewSession() require.NoError(t, err) defer session.Close() - require.NoError(t, session.Run("echo test")) + stdin, err := session.StdinPipe() + require.NoError(t, err) + err = session.Shell() + require.NoError(t, err) var s *agentsdk.Stats require.Eventuallyf(t, func() bool { @@ -78,6 +81,9 @@ func TestAgent_Stats_SSH(t *testing.T) { }, testutil.WaitLong, testutil.IntervalFast, "never saw stats: %+v", s, ) + _ = stdin.Close() + err = session.Wait() + require.NoError(t, err) } func TestAgent_Stats_ReconnectingPTY(t *testing.T) { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 005694ea4c9ea..096ae60d2424d 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -554,12 +554,12 @@ type WorkspaceConnectionLatencyMS struct { type DeploymentStats struct { // AggregatedFrom is the time in which stats are aggregated from. // This might be back in time a specific duration or interval. - AggregatedFrom time.Time `json:"aggregated_since"` + AggregatedFrom time.Time `json:"aggregated_since" format:"date-time"` // CollectedAt is the time in which stats are collected at. - CollectedAt time.Time `json:"collected_at"` + CollectedAt time.Time `json:"collected_at" format:"date-time"` // RefreshingAt is the time when the next batch of stats will // be refreshed. - RefreshingAt time.Time `json:"refreshing_at"` + RefreshingAt time.Time `json:"refreshing_at" format:"date-time"` PendingWorkspaces int64 `json:"pending_workspaces"` BuildingWorkspaces int64 `json:"building_workspaces"` From e87ba5978aa5ee2076ba96ddf72c02921cd467b4 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 16:58:48 +0000 Subject: [PATCH 19/24] Fix gen --- agent/agent.go | 1 - coderd/apidoc/docs.go | 9 ++++++--- coderd/apidoc/swagger.json | 9 ++++++--- coderd/database/dbfake/databasefake.go | 14 +++++++++----- docs/api/general.md | 6 +++--- docs/api/schemas.md | 6 +++--- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index f59fa1027ad4e..7732ad6e80079 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1272,7 +1272,6 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { a.logger.Info(ctx, "skipping stat because nothing changed") return } - a.latestStat.Store(stats) select { case a.connStatsChan <- stats: diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e8830bced6741..d4530d380d73a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6685,14 +6685,16 @@ const docTemplate = `{ "properties": { "aggregated_since": { "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", - "type": "string" + "type": "string", + "format": "date-time" }, "building_workspaces": { "type": "integer" }, "collected_at": { "description": "CollectedAt is the time in which stats are collected at.", - "type": "string" + "type": "string", + "format": "date-time" }, "failed_workspaces": { "type": "integer" @@ -6702,7 +6704,8 @@ const docTemplate = `{ }, "refreshing_at": { "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", - "type": "string" + "type": "string", + "format": "date-time" }, "running_workspaces": { "type": "integer" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 23afea9012ab8..f9b9126379410 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5985,14 +5985,16 @@ "properties": { "aggregated_since": { "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", - "type": "string" + "type": "string", + "format": "date-time" }, "building_workspaces": { "type": "integer" }, "collected_at": { "description": "CollectedAt is the time in which stats are collected at.", - "type": "string" + "type": "string", + "format": "date-time" }, "failed_workspaces": { "type": "integer" @@ -6002,7 +6004,8 @@ }, "refreshing_at": { "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", - "type": "string" + "type": "string", + "format": "date-time" }, "running_workspaces": { "type": "integer" diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 08f4bfd94d5d4..2c0864de68617 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -293,7 +293,7 @@ func (q *fakeQuerier) GetDeploymentWorkspaceAgentStats(_ context.Context, create for _, agentStat := range agentStatsCreatedAfter { stat.WorkspaceRxBytes += agentStat.RxBytes stat.WorkspaceTxBytes += agentStat.TxBytes - latencies = append(latencies, float64(agentStat.ConnectionMedianLatencyMS)) + latencies = append(latencies, agentStat.ConnectionMedianLatencyMS) } tryPercentile := func(fs []float64, p float64) float64 { @@ -1029,7 +1029,7 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } if arg.Status != "" { - build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) if err != nil { return nil, xerrors.Errorf("get latest build: %w", err) } @@ -1118,7 +1118,7 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } if arg.HasAgent != "" { - build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) if err != nil { return nil, xerrors.Errorf("get latest build: %w", err) } @@ -1424,10 +1424,14 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() + return q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) +} + +func (q *fakeQuerier) getLatestWorkspaceBuildByWorkspaceIDNoLock(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { var row database.WorkspaceBuild var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { @@ -3612,7 +3616,7 @@ func (q *fakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database stat := database.GetDeploymentWorkspaceStatsRow{} for _, workspace := range q.workspaces { - build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID) if err != nil { return stat, err } diff --git a/docs/api/general.md b/docs/api/general.md index 56c1bb606da92..540463dbc6086 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -1186,12 +1186,12 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \ ```json { - "aggregated_since": "string", + "aggregated_since": "2019-08-24T14:15:22Z", "building_workspaces": 0, - "collected_at": "string", + "collected_at": "2019-08-24T14:15:22Z", "failed_workspaces": 0, "pending_workspaces": 0, - "refreshing_at": "string", + "refreshing_at": "2019-08-24T14:15:22Z", "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index f0d849ce243f4..7e3cc6d929963 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2931,12 +2931,12 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { - "aggregated_since": "string", + "aggregated_since": "2019-08-24T14:15:22Z", "building_workspaces": 0, - "collected_at": "string", + "collected_at": "2019-08-24T14:15:22Z", "failed_workspaces": 0, "pending_workspaces": 0, - "refreshing_at": "string", + "refreshing_at": "2019-08-24T14:15:22Z", "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, From 99d7d1a33eb9f1453c9046916a564db802fa2a2c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 17:59:49 +0000 Subject: [PATCH 20/24] Fix migrations --- .../migrations/000102_workspace_agent_stats_types.up.sql | 2 +- .../000107_workspace_agent_stats_connection_latency.down.sql | 1 + .../000107_workspace_agent_stats_connection_latency.up.sql | 1 + coderd/database/querier_test.go | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql create mode 100644 coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql diff --git a/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql b/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql index 421d5478e977a..39effadfee42c 100644 --- a/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql +++ b/coderd/database/migrations/000102_workspace_agent_stats_types.up.sql @@ -1,4 +1,4 @@ -ALTER TABLE workspace_agent_stats ADD COLUMN connection_median_latency_ms float DEFAULT -1 NOT NULL; +ALTER TABLE workspace_agent_stats ADD COLUMN connection_median_latency_ms bigint DEFAULT -1 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_vscode bigint DEFAULT 0 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_jetbrains bigint DEFAULT 0 NOT NULL; ALTER TABLE workspace_agent_stats ADD COLUMN session_count_reconnecting_pty bigint DEFAULT 0 NOT NULL; diff --git a/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql b/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql new file mode 100644 index 0000000000000..edbde53b237b0 --- /dev/null +++ b/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_agent_stats ALTER COLUMN connection_median_latency_ms TYPE bigint; diff --git a/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql b/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql new file mode 100644 index 0000000000000..c8ccaa7768015 --- /dev/null +++ b/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_agent_stats ALTER COLUMN connection_median_latency_ms TYPE FLOAT; diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 8ed967e8fcf52..5e8def562c16d 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1,3 +1,5 @@ +//go:build linux + package database_test import ( From 37ad03fd89641b30d89fdd642cc887f219a5e82b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Mar 2023 19:02:01 +0000 Subject: [PATCH 21/24] Block on sending stat updates --- agent/agent.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 7732ad6e80079..2c209fe602d36 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1272,14 +1272,11 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) { a.logger.Info(ctx, "skipping stat because nothing changed") return } + a.latestStat.Store(stats) select { case a.connStatsChan <- stats: - // Only store the latest stat when it's successfully sent! - // Otherwise, it should be sent again on the next iteration. - a.latestStat.Store(stats) - default: - a.logger.Warn(ctx, "network stat dropped") + case <-a.closed: } } From 0037a64fca2f569ae16ac573bedb13e17fa2a64d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 8 Mar 2023 17:31:54 +0000 Subject: [PATCH 22/24] Add test fixtures --- ...kspace_agent_stats_connection_latency.up.sql | 17 +++++++++++++++++ ...kspace_agent_stats_connection_latency.up.sql | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 coderd/database/migrations/testdata/fixtures/000106_pre_workspace_agent_stats_connection_latency.up.sql create mode 100644 coderd/database/migrations/testdata/fixtures/000107_post_workspace_agent_stats_connection_latency.up.sql diff --git a/coderd/database/migrations/testdata/fixtures/000106_pre_workspace_agent_stats_connection_latency.up.sql b/coderd/database/migrations/testdata/fixtures/000106_pre_workspace_agent_stats_connection_latency.up.sql new file mode 100644 index 0000000000000..daf39cb2660d8 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000106_pre_workspace_agent_stats_connection_latency.up.sql @@ -0,0 +1,17 @@ +INSERT INTO workspace_agent_stats ( + id, + created_at, + user_id, + agent_id, + workspace_id, + template_id, + connection_median_latency_ms +) VALUES ( + gen_random_uuid(), + NOW(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + 1::bigint +); diff --git a/coderd/database/migrations/testdata/fixtures/000107_post_workspace_agent_stats_connection_latency.up.sql b/coderd/database/migrations/testdata/fixtures/000107_post_workspace_agent_stats_connection_latency.up.sql new file mode 100644 index 0000000000000..58b8f248b9c39 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000107_post_workspace_agent_stats_connection_latency.up.sql @@ -0,0 +1,17 @@ +INSERT INTO workspace_agent_stats ( + id, + created_at, + user_id, + agent_id, + workspace_id, + template_id, + connection_median_latency_ms +) VALUES ( + gen_random_uuid(), + NOW(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + gen_random_uuid(), + 0.5::float +); From d7082109b8f3e750a202bee18b893904bcb79cf9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 9 Mar 2023 02:34:01 +0000 Subject: [PATCH 23/24] Fix response structure --- coderd/apidoc/docs.go | 12 ++--- coderd/apidoc/swagger.json | 12 ++--- ...e_agent_stats_connection_latency.down.sql} | 0 ...ace_agent_stats_connection_latency.up.sql} | 0 coderd/metricscache/metricscache.go | 35 ++++++++------- coderd/metricscache/metricscache_test.go | 2 +- codersdk/deployment.go | 44 +++++++++++-------- docs/api/general.md | 4 +- docs/api/schemas.md | 38 ++++++++-------- site/src/api/typesGenerated.ts | 38 ++++++++++------ .../DeploymentBanner/DeploymentBanner.tsx | 2 +- .../DeploymentBanner/DeploymentBannerView.tsx | 32 +++++++------- site/src/testHelpers/entities.ts | 36 ++++++++------- 13 files changed, 139 insertions(+), 116 deletions(-) rename coderd/database/migrations/{000107_workspace_agent_stats_connection_latency.down.sql => 000108_workspace_agent_stats_connection_latency.down.sql} (100%) rename coderd/database/migrations/{000107_workspace_agent_stats_connection_latency.up.sql => 000108_workspace_agent_stats_connection_latency.up.sql} (100%) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ce63f2ea87278..81ddb2ba78629 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6482,7 +6482,7 @@ const docTemplate = `{ "codersdk.DeploymentStats": { "type": "object", "properties": { - "aggregated_since": { + "aggregated_from": { "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", "type": "string", "format": "date-time" @@ -6498,14 +6498,14 @@ const docTemplate = `{ "failed_workspaces": { "type": "integer" }, - "pending_workspaces": { - "type": "integer" - }, - "refreshing_at": { - "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", + "next_update_at": { + "description": "NextUpdateAt is the time when the next batch of stats will\nbe updated.", "type": "string", "format": "date-time" }, + "pending_workspaces": { + "type": "integer" + }, "running_workspaces": { "type": "integer" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 9bea8f9add29e..c274d19288f81 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5782,7 +5782,7 @@ "codersdk.DeploymentStats": { "type": "object", "properties": { - "aggregated_since": { + "aggregated_from": { "description": "AggregatedFrom is the time in which stats are aggregated from.\nThis might be back in time a specific duration or interval.", "type": "string", "format": "date-time" @@ -5798,14 +5798,14 @@ "failed_workspaces": { "type": "integer" }, - "pending_workspaces": { - "type": "integer" - }, - "refreshing_at": { - "description": "RefreshingAt is the time when the next batch of stats will\nbe refreshed.", + "next_update_at": { + "description": "NextUpdateAt is the time when the next batch of stats will\nbe updated.", "type": "string", "format": "date-time" }, + "pending_workspaces": { + "type": "integer" + }, "running_workspaces": { "type": "integer" }, diff --git a/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql b/coderd/database/migrations/000108_workspace_agent_stats_connection_latency.down.sql similarity index 100% rename from coderd/database/migrations/000107_workspace_agent_stats_connection_latency.down.sql rename to coderd/database/migrations/000108_workspace_agent_stats_connection_latency.down.sql diff --git a/coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql b/coderd/database/migrations/000108_workspace_agent_stats_connection_latency.up.sql similarity index 100% rename from coderd/database/migrations/000107_workspace_agent_stats_connection_latency.up.sql rename to coderd/database/migrations/000108_workspace_agent_stats_connection_latency.up.sql diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 52b769087335e..f65d82dd62a02 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -235,23 +235,26 @@ func (c *Cache) refreshDeploymentStats(ctx context.Context) error { c.deploymentStatsResponse.Store(&codersdk.DeploymentStats{ AggregatedFrom: from, CollectedAt: database.Now(), - RefreshingAt: database.Now().Add(c.intervals.DeploymentStats), - WorkspaceConnectionLatencyMS: codersdk.WorkspaceConnectionLatencyMS{ - P50: agentStats.WorkspaceConnectionLatency50, - P95: agentStats.WorkspaceConnectionLatency95, + NextUpdateAt: database.Now().Add(c.intervals.DeploymentStats), + Workspaces: codersdk.WorkspaceDeploymentStats{ + Pending: workspaceStats.PendingWorkspaces, + Building: workspaceStats.BuildingWorkspaces, + Running: workspaceStats.RunningWorkspaces, + Failed: workspaceStats.FailedWorkspaces, + Stopped: workspaceStats.StoppedWorkspaces, + ConnectionLatencyMS: codersdk.WorkspaceConnectionLatencyMS{ + P50: agentStats.WorkspaceConnectionLatency50, + P95: agentStats.WorkspaceConnectionLatency95, + }, + RxBytes: agentStats.WorkspaceRxBytes, + TxBytes: agentStats.WorkspaceTxBytes, + }, + SessionCount: codersdk.SessionCountDeploymentStats{ + VSCode: agentStats.SessionCountVSCode, + SSH: agentStats.SessionCountSSH, + JetBrains: agentStats.SessionCountJetBrains, + ReconnectingPTY: agentStats.SessionCountReconnectingPTY, }, - SessionCountVSCode: agentStats.SessionCountVSCode, - SessionCountSSH: agentStats.SessionCountSSH, - SessionCountJetBrains: agentStats.SessionCountJetBrains, - SessionCountReconnectingPTY: agentStats.SessionCountReconnectingPTY, - WorkspaceRxBytes: agentStats.WorkspaceRxBytes, - WorkspaceTxBytes: agentStats.WorkspaceTxBytes, - - PendingWorkspaces: workspaceStats.PendingWorkspaces, - BuildingWorkspaces: workspaceStats.BuildingWorkspaces, - RunningWorkspaces: workspaceStats.RunningWorkspaces, - FailedWorkspaces: workspaceStats.FailedWorkspaces, - StoppedWorkspaces: workspaceStats.StoppedWorkspaces, }) return nil } diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 94cc7cab7c776..90d5e93e1d750 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -399,5 +399,5 @@ func TestCache_DeploymentStats(t *testing.T) { stat, ok = cache.DeploymentStats() return ok }, testutil.WaitLong, testutil.IntervalMedium) - require.Equal(t, int64(1), stat.SessionCountVSCode) + require.Equal(t, int64(1), stat.SessionCount.VSCode) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9fd43e613d4fa..5066d9ff11f45 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1532,29 +1532,35 @@ type WorkspaceConnectionLatencyMS struct { P95 float64 } +type WorkspaceDeploymentStats struct { + Pending int64 `json:"pending"` + Building int64 `json:"building"` + Running int64 `json:"running"` + Failed int64 `json:"failed"` + Stopped int64 `json:"stopped"` + + ConnectionLatencyMS WorkspaceConnectionLatencyMS `json:"connection_latency_ms"` + RxBytes int64 `json:"rx_bytes"` + TxBytes int64 `json:"tx_bytes"` +} + +type SessionCountDeploymentStats struct { + VSCode int64 `json:"vscode"` + SSH int64 `json:"ssh"` + JetBrains int64 `json:"jetbrains"` + ReconnectingPTY int64 `json:"reconnecting_pty"` +} + type DeploymentStats struct { // AggregatedFrom is the time in which stats are aggregated from. // This might be back in time a specific duration or interval. - AggregatedFrom time.Time `json:"aggregated_since" format:"date-time"` + AggregatedFrom time.Time `json:"aggregated_from" format:"date-time"` // CollectedAt is the time in which stats are collected at. CollectedAt time.Time `json:"collected_at" format:"date-time"` - // RefreshingAt is the time when the next batch of stats will - // be refreshed. - RefreshingAt time.Time `json:"refreshing_at" format:"date-time"` - - PendingWorkspaces int64 `json:"pending_workspaces"` - BuildingWorkspaces int64 `json:"building_workspaces"` - RunningWorkspaces int64 `json:"running_workspaces"` - FailedWorkspaces int64 `json:"failed_workspaces"` - StoppedWorkspaces int64 `json:"stopped_workspaces"` - - WorkspaceConnectionLatencyMS WorkspaceConnectionLatencyMS `json:"workspace_connection_latency_ms"` - - SessionCountVSCode int64 `json:"session_count_vscode"` - SessionCountSSH int64 `json:"session_count_ssh"` - SessionCountJetBrains int64 `json:"session_count_jetbrains"` - SessionCountReconnectingPTY int64 `json:"session_count_reconnecting_pty"` + // NextUpdateAt is the time when the next batch of stats will + // be updated. + NextUpdateAt time.Time `json:"next_update_at" format:"date-time"` - WorkspaceRxBytes int64 `json:"workspace_rx_bytes"` - WorkspaceTxBytes int64 `json:"workspace_tx_bytes"` + Workspaces WorkspaceDeploymentStats `json:"workspaces"` + SessionCount SessionCountDeploymentStats `json:"session_count"` } diff --git a/docs/api/general.md b/docs/api/general.md index b835aceee6647..4a010ef8877e4 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -416,12 +416,12 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \ ```json { - "aggregated_since": "2019-08-24T14:15:22Z", + "aggregated_from": "2019-08-24T14:15:22Z", "building_workspaces": 0, "collected_at": "2019-08-24T14:15:22Z", "failed_workspaces": 0, + "next_update_at": "2019-08-24T14:15:22Z", "pending_workspaces": 0, - "refreshing_at": "2019-08-24T14:15:22Z", "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index c622c531e3ab1..0b3599d07e74c 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1951,12 +1951,12 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { - "aggregated_since": "2019-08-24T14:15:22Z", + "aggregated_from": "2019-08-24T14:15:22Z", "building_workspaces": 0, "collected_at": "2019-08-24T14:15:22Z", "failed_workspaces": 0, + "next_update_at": "2019-08-24T14:15:22Z", "pending_workspaces": 0, - "refreshing_at": "2019-08-24T14:15:22Z", "running_workspaces": 0, "session_count_jetbrains": 0, "session_count_reconnecting_pty": 0, @@ -1974,23 +1974,23 @@ CreateParameterRequest is a structure used to create a new parameter value for a ### Properties -| Name | Type | Required | Restrictions | Description | -| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| `aggregated_since` | string | false | | Aggregated since is the time in which stats are aggregated from. This might be back in time a specific duration or interval. | -| `building_workspaces` | integer | false | | | -| `collected_at` | string | false | | Collected at is the time in which stats are collected at. | -| `failed_workspaces` | integer | false | | | -| `pending_workspaces` | integer | false | | | -| `refreshing_at` | string | false | | Refreshing at is the time when the next batch of stats will be refreshed. | -| `running_workspaces` | integer | false | | | -| `session_count_jetbrains` | integer | false | | | -| `session_count_reconnecting_pty` | integer | false | | | -| `session_count_ssh` | integer | false | | | -| `session_count_vscode` | integer | false | | | -| `stopped_workspaces` | integer | false | | | -| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | -| `workspace_rx_bytes` | integer | false | | | -| `workspace_tx_bytes` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `aggregated_from` | string | false | | Aggregated from is the time in which stats are aggregated from. This might be back in time a specific duration or interval. | +| `building_workspaces` | integer | false | | | +| `collected_at` | string | false | | Collected at is the time in which stats are collected at. | +| `failed_workspaces` | integer | false | | | +| `next_update_at` | string | false | | Next update at is the time when the next batch of stats will be updated. | +| `pending_workspaces` | integer | false | | | +| `running_workspaces` | integer | false | | | +| `session_count_jetbrains` | integer | false | | | +| `session_count_reconnecting_pty` | integer | false | | | +| `session_count_ssh` | integer | false | | | +| `session_count_vscode` | integer | false | | | +| `stopped_workspaces` | integer | false | | | +| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | +| `workspace_rx_bytes` | integer | false | | | +| `workspace_tx_bytes` | integer | false | | | ## codersdk.DeploymentValues diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index dbfadf0507c61..cc7faba89cf34 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -314,21 +314,11 @@ export interface DeploymentDAUsResponse { // From codersdk/deployment.go export interface DeploymentStats { - readonly aggregated_since: string + readonly aggregated_from: string readonly collected_at: string - readonly refreshing_at: string - readonly pending_workspaces: number - readonly building_workspaces: number - readonly running_workspaces: number - readonly failed_workspaces: number - readonly stopped_workspaces: number - readonly workspace_connection_latency_ms: WorkspaceConnectionLatencyMS - readonly session_count_vscode: number - readonly session_count_ssh: number - readonly session_count_jetbrains: number - readonly session_count_reconnecting_pty: number - readonly workspace_rx_bytes: number - readonly workspace_tx_bytes: number + readonly next_update_at: string + readonly workspaces: WorkspaceDeploymentStats + readonly session_count: SessionCountDeploymentStats } // From codersdk/deployment.go @@ -753,6 +743,14 @@ export interface ServiceBannerConfig { readonly background_color?: string } +// From codersdk/deployment.go +export interface SessionCountDeploymentStats { + readonly vscode: number + readonly ssh: number + readonly jetbrains: number + readonly reconnecting_pty: number +} + // From codersdk/deployment.go export interface SupportConfig { // Named type "github.com/coder/coder/cli/clibase.Struct[[]github.com/coder/coder/codersdk.LinkConfig]" unknown, using "any" @@ -1169,6 +1167,18 @@ export interface WorkspaceConnectionLatencyMS { readonly P95: number } +// From codersdk/deployment.go +export interface WorkspaceDeploymentStats { + readonly pending: number + readonly building: number + readonly running: number + readonly failed: number + readonly stopped: number + readonly connection_latency_ms: WorkspaceConnectionLatencyMS + readonly rx_bytes: number + readonly tx_bytes: number +} + // From codersdk/workspaces.go export interface WorkspaceFilter { readonly q?: string diff --git a/site/src/components/DeploymentBanner/DeploymentBanner.tsx b/site/src/components/DeploymentBanner/DeploymentBanner.tsx index 2c1bcd7ec5074..d5cd1e25e5390 100644 --- a/site/src/components/DeploymentBanner/DeploymentBanner.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBanner.tsx @@ -7,7 +7,7 @@ export const DeploymentBanner: React.FC = () => { const permissions = usePermissions() const [state, sendEvent] = useMachine(deploymentStatsMachine) - if (!permissions.viewDeploymentConfig || !state.context.deploymentStats) { + if (!permissions.viewDeploymentValues || !state.context.deploymentStats) { return null } diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx index c9202e58811bf..a5c80b5d58f2f 100644 --- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx @@ -37,16 +37,16 @@ export const DeploymentBannerView: FC = ({ if (!stats) { return } - return dayjs(stats.collected_at).diff(stats.aggregated_since, "minutes") + return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes") }, [stats]) - const displayLatency = stats?.workspace_connection_latency_ms.P50 || -1 + const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1 const [timeUntilRefresh, setTimeUntilRefresh] = useState(0) useEffect(() => { if (!stats || !fetchStats) { return } - let timeUntilRefresh = dayjs(stats.refreshing_at).diff( + let timeUntilRefresh = dayjs(stats.next_update_at).diff( stats.collected_at, "seconds", ) @@ -92,27 +92,27 @@ export const DeploymentBannerView: FC = ({
@@ -128,14 +128,14 @@ export const DeploymentBannerView: FC = ({
- {stats ? prettyBytes(stats.workspace_rx_bytes) : "-"} + {stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"}
- {stats ? prettyBytes(stats.workspace_tx_bytes) : "-"} + {stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"}
@@ -160,27 +160,27 @@ export const DeploymentBannerView: FC = ({
- {typeof stats?.session_count_vscode === "undefined" + {typeof stats?.session_count.vscode === "undefined" ? "-" - : stats?.session_count_vscode} + : stats?.session_count.vscode}
- {typeof stats?.session_count_ssh === "undefined" + {typeof stats?.session_count.ssh === "undefined" ? "-" - : stats?.session_count_ssh} + : stats?.session_count.ssh}
- {typeof stats?.session_count_reconnecting_pty === "undefined" + {typeof stats?.session_count.reconnecting_pty === "undefined" ? "-" - : stats?.session_count_reconnecting_pty} + : stats?.session_count.reconnecting_pty}
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index fe5cf2dd0b60d..01a2ab60cc090 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1539,22 +1539,26 @@ export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = { } export const MockDeploymentStats: TypesGen.DeploymentStats = { - aggregated_since: "2023-03-06T19:08:55.211625Z", + aggregated_from: "2023-03-06T19:08:55.211625Z", collected_at: "2023-03-06T19:12:55.211625Z", - refreshing_at: "2023-03-06T19:20:55.211625Z", - session_count_jetbrains: 5, - session_count_reconnecting_pty: 15, - session_count_ssh: 32, - session_count_vscode: 128, - workspace_connection_latency_ms: { - P50: 32.56, - P95: 15.23, + next_update_at: "2023-03-06T19:20:55.211625Z", + session_count: { + vscode: 128, + jetbrains: 5, + ssh: 32, + reconnecting_pty: 15, + }, + workspaces: { + building: 15, + failed: 12, + pending: 5, + running: 32, + stopped: 16, + connection_latency_ms: { + P50: 32.56, + P95: 15.23, + }, + rx_bytes: 15613513253, + tx_bytes: 36113513253, }, - workspace_rx_bytes: 15613513253, - workspace_tx_bytes: 36113513253, - building_workspaces: 15, - failed_workspaces: 12, - pending_workspaces: 5, - running_workspaces: 32, - stopped_workspaces: 16, } From c951d5ab6ebc4b0ad4e31597b213f266facfd92f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 9 Mar 2023 02:56:26 +0000 Subject: [PATCH 24/24] make gen --- coderd/apidoc/docs.go | 84 +++++++++++++++++------------ coderd/apidoc/swagger.json | 84 +++++++++++++++++------------ docs/api/general.md | 32 ++++++----- docs/api/schemas.md | 107 ++++++++++++++++++++++++++----------- 4 files changed, 194 insertions(+), 113 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 81ddb2ba78629..966bff2bf4d45 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6487,51 +6487,21 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, - "building_workspaces": { - "type": "integer" - }, "collected_at": { "description": "CollectedAt is the time in which stats are collected at.", "type": "string", "format": "date-time" }, - "failed_workspaces": { - "type": "integer" - }, "next_update_at": { "description": "NextUpdateAt is the time when the next batch of stats will\nbe updated.", "type": "string", "format": "date-time" }, - "pending_workspaces": { - "type": "integer" - }, - "running_workspaces": { - "type": "integer" - }, - "session_count_jetbrains": { - "type": "integer" - }, - "session_count_reconnecting_pty": { - "type": "integer" - }, - "session_count_ssh": { - "type": "integer" + "session_count": { + "$ref": "#/definitions/codersdk.SessionCountDeploymentStats" }, - "session_count_vscode": { - "type": "integer" - }, - "stopped_workspaces": { - "type": "integer" - }, - "workspace_connection_latency_ms": { - "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" - }, - "workspace_rx_bytes": { - "type": "integer" - }, - "workspace_tx_bytes": { - "type": "integer" + "workspaces": { + "$ref": "#/definitions/codersdk.WorkspaceDeploymentStats" } } }, @@ -7695,6 +7665,23 @@ const docTemplate = `{ } } }, + "codersdk.SessionCountDeploymentStats": { + "type": "object", + "properties": { + "jetbrains": { + "type": "integer" + }, + "reconnecting_pty": { + "type": "integer" + }, + "ssh": { + "type": "integer" + }, + "vscode": { + "type": "integer" + } + } + }, "codersdk.SupportConfig": { "type": "object", "properties": { @@ -8843,6 +8830,35 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceDeploymentStats": { + "type": "object", + "properties": { + "building": { + "type": "integer" + }, + "connection_latency_ms": { + "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" + }, + "failed": { + "type": "integer" + }, + "pending": { + "type": "integer" + }, + "running": { + "type": "integer" + }, + "rx_bytes": { + "type": "integer" + }, + "stopped": { + "type": "integer" + }, + "tx_bytes": { + "type": "integer" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c274d19288f81..b64e2cd4d79d2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5787,51 +5787,21 @@ "type": "string", "format": "date-time" }, - "building_workspaces": { - "type": "integer" - }, "collected_at": { "description": "CollectedAt is the time in which stats are collected at.", "type": "string", "format": "date-time" }, - "failed_workspaces": { - "type": "integer" - }, "next_update_at": { "description": "NextUpdateAt is the time when the next batch of stats will\nbe updated.", "type": "string", "format": "date-time" }, - "pending_workspaces": { - "type": "integer" - }, - "running_workspaces": { - "type": "integer" - }, - "session_count_jetbrains": { - "type": "integer" - }, - "session_count_reconnecting_pty": { - "type": "integer" - }, - "session_count_ssh": { - "type": "integer" + "session_count": { + "$ref": "#/definitions/codersdk.SessionCountDeploymentStats" }, - "session_count_vscode": { - "type": "integer" - }, - "stopped_workspaces": { - "type": "integer" - }, - "workspace_connection_latency_ms": { - "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" - }, - "workspace_rx_bytes": { - "type": "integer" - }, - "workspace_tx_bytes": { - "type": "integer" + "workspaces": { + "$ref": "#/definitions/codersdk.WorkspaceDeploymentStats" } } }, @@ -6906,6 +6876,23 @@ } } }, + "codersdk.SessionCountDeploymentStats": { + "type": "object", + "properties": { + "jetbrains": { + "type": "integer" + }, + "reconnecting_pty": { + "type": "integer" + }, + "ssh": { + "type": "integer" + }, + "vscode": { + "type": "integer" + } + } + }, "codersdk.SupportConfig": { "type": "object", "properties": { @@ -7974,6 +7961,35 @@ } } }, + "codersdk.WorkspaceDeploymentStats": { + "type": "object", + "properties": { + "building": { + "type": "integer" + }, + "connection_latency_ms": { + "$ref": "#/definitions/codersdk.WorkspaceConnectionLatencyMS" + }, + "failed": { + "type": "integer" + }, + "pending": { + "type": "integer" + }, + "running": { + "type": "integer" + }, + "rx_bytes": { + "type": "integer" + }, + "stopped": { + "type": "integer" + }, + "tx_bytes": { + "type": "integer" + } + } + }, "codersdk.WorkspaceQuota": { "type": "object", "properties": { diff --git a/docs/api/general.md b/docs/api/general.md index 4a010ef8877e4..2516d79e995c2 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -417,23 +417,27 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \ ```json { "aggregated_from": "2019-08-24T14:15:22Z", - "building_workspaces": 0, "collected_at": "2019-08-24T14:15:22Z", - "failed_workspaces": 0, "next_update_at": "2019-08-24T14:15:22Z", - "pending_workspaces": 0, - "running_workspaces": 0, - "session_count_jetbrains": 0, - "session_count_reconnecting_pty": 0, - "session_count_ssh": 0, - "session_count_vscode": 0, - "stopped_workspaces": 0, - "workspace_connection_latency_ms": { - "p50": 0, - "p95": 0 + "session_count": { + "jetbrains": 0, + "reconnecting_pty": 0, + "ssh": 0, + "vscode": 0 }, - "workspace_rx_bytes": 0, - "workspace_tx_bytes": 0 + "workspaces": { + "building": 0, + "connection_latency_ms": { + "p50": 0, + "p95": 0 + }, + "failed": 0, + "pending": 0, + "running": 0, + "rx_bytes": 0, + "stopped": 0, + "tx_bytes": 0 + } } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 0b3599d07e74c..bfe21043b84b9 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1952,45 +1952,39 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { "aggregated_from": "2019-08-24T14:15:22Z", - "building_workspaces": 0, "collected_at": "2019-08-24T14:15:22Z", - "failed_workspaces": 0, "next_update_at": "2019-08-24T14:15:22Z", - "pending_workspaces": 0, - "running_workspaces": 0, - "session_count_jetbrains": 0, - "session_count_reconnecting_pty": 0, - "session_count_ssh": 0, - "session_count_vscode": 0, - "stopped_workspaces": 0, - "workspace_connection_latency_ms": { - "p50": 0, - "p95": 0 + "session_count": { + "jetbrains": 0, + "reconnecting_pty": 0, + "ssh": 0, + "vscode": 0 }, - "workspace_rx_bytes": 0, - "workspace_tx_bytes": 0 + "workspaces": { + "building": 0, + "connection_latency_ms": { + "p50": 0, + "p95": 0 + }, + "failed": 0, + "pending": 0, + "running": 0, + "rx_bytes": 0, + "stopped": 0, + "tx_bytes": 0 + } } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| --------------------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- | -| `aggregated_from` | string | false | | Aggregated from is the time in which stats are aggregated from. This might be back in time a specific duration or interval. | -| `building_workspaces` | integer | false | | | -| `collected_at` | string | false | | Collected at is the time in which stats are collected at. | -| `failed_workspaces` | integer | false | | | -| `next_update_at` | string | false | | Next update at is the time when the next batch of stats will be updated. | -| `pending_workspaces` | integer | false | | | -| `running_workspaces` | integer | false | | | -| `session_count_jetbrains` | integer | false | | | -| `session_count_reconnecting_pty` | integer | false | | | -| `session_count_ssh` | integer | false | | | -| `session_count_vscode` | integer | false | | | -| `stopped_workspaces` | integer | false | | | -| `workspace_connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | -| `workspace_rx_bytes` | integer | false | | | -| `workspace_tx_bytes` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------------- | ---------------------------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `aggregated_from` | string | false | | Aggregated from is the time in which stats are aggregated from. This might be back in time a specific duration or interval. | +| `collected_at` | string | false | | Collected at is the time in which stats are collected at. | +| `next_update_at` | string | false | | Next update at is the time when the next batch of stats will be updated. | +| `session_count` | [codersdk.SessionCountDeploymentStats](#codersdksessioncountdeploymentstats) | false | | | +| `workspaces` | [codersdk.WorkspaceDeploymentStats](#codersdkworkspacedeploymentstats) | false | | | ## codersdk.DeploymentValues @@ -3338,6 +3332,26 @@ Parameter represents a set value for the scope. | `enabled` | boolean | false | | | | `message` | string | false | | | +## codersdk.SessionCountDeploymentStats + +```json +{ + "jetbrains": 0, + "reconnecting_pty": 0, + "ssh": 0, + "vscode": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | ------- | -------- | ------------ | ----------- | +| `jetbrains` | integer | false | | | +| `reconnecting_pty` | integer | false | | | +| `ssh` | integer | false | | | +| `vscode` | integer | false | | | + ## codersdk.SupportConfig ```json @@ -4807,6 +4821,37 @@ Parameter represents a set value for the scope. | `p50` | number | false | | | | `p95` | number | false | | | +## codersdk.WorkspaceDeploymentStats + +```json +{ + "building": 0, + "connection_latency_ms": { + "p50": 0, + "p95": 0 + }, + "failed": 0, + "pending": 0, + "running": 0, + "rx_bytes": 0, + "stopped": 0, + "tx_bytes": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----------------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `building` | integer | false | | | +| `connection_latency_ms` | [codersdk.WorkspaceConnectionLatencyMS](#codersdkworkspaceconnectionlatencyms) | false | | | +| `failed` | integer | false | | | +| `pending` | integer | false | | | +| `running` | integer | false | | | +| `rx_bytes` | integer | false | | | +| `stopped` | integer | false | | | +| `tx_bytes` | integer | false | | | + ## codersdk.WorkspaceQuota ```json