From 83db44dee1e3f0016fe8050b42fd354252b462b4 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 14 Jun 2024 19:52:42 +0000 Subject: [PATCH 01/14] chore: stop saving session stats with experiment --- coderd/agentapi/stats.go | 12 +++++ coderd/agentapi/stats_test.go | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/coderd/agentapi/stats.go b/coderd/agentapi/stats.go index a167fb5d6f275..9998fa9275534 100644 --- a/coderd/agentapi/stats.go +++ b/coderd/agentapi/stats.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/codersdk" ) type StatsAPI struct { @@ -20,6 +21,7 @@ type StatsAPI struct { Log slog.Logger StatsReporter *workspacestats.Reporter AgentStatsRefreshInterval time.Duration + Experiments codersdk.Experiments TimeNowFn func() time.Time // defaults to dbtime.Now() } @@ -55,6 +57,16 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR slog.F("payload", req), ) + if a.Experiments.Enabled(codersdk.ExperimentWorkspaceUsage) { + // Session agent stats are being handled by postWorkspaceUsage route when this + // experiment is enabled. We still want most of the stats data but will zero + // out the ones being written elsewhere. + req.Stats.SessionCountVscode = 0 + req.Stats.SessionCountJetbrains = 0 + req.Stats.SessionCountReconnectingPty = 0 + req.Stats.SessionCountSsh = 0 + } + err = a.StatsReporter.ReportAgentStats( ctx, a.now(), diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index 8b4d72fc1d579..53560a2d751d5 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -406,6 +406,104 @@ func TestUpdateStates(t *testing.T) { require.True(t, updateAgentMetricsFnCalled) }) + + t.Run("WorkspaceUsageExperiment", func(t *testing.T) { + t.Parallel() + + var ( + now = dbtime.Now() + dbM = dbmock.NewMockStore(gomock.NewController(t)) + ps = pubsub.NewInMemory() + + templateScheduleStore = schedule.MockTemplateScheduleStore{ + GetFn: func(context.Context, database.Store, uuid.UUID) (schedule.TemplateScheduleOptions, error) { + panic("should not be called") + }, + SetFn: func(context.Context, database.Store, database.Template, schedule.TemplateScheduleOptions) (database.Template, error) { + panic("not implemented") + }, + } + batcher = &statsBatcher{} + + req = &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{ + "tcp": 1, + "dean": 2, + }, + ConnectionCount: 3, + ConnectionMedianLatencyMs: 23, + RxPackets: 120, + RxBytes: 1000, + TxPackets: 130, + TxBytes: 2000, + SessionCountVscode: 1, + SessionCountJetbrains: 2, + SessionCountReconnectingPty: 3, + SessionCountSsh: 4, + Metrics: []*agentproto.Stats_Metric{ + { + Name: "awesome metric", + Value: 42, + }, + { + Name: "uncool metric", + Value: 0, + }, + }, + }, + } + ) + api := agentapi.StatsAPI{ + AgentFn: func(context.Context) (database.WorkspaceAgent, error) { + return agent, nil + }, + Database: dbM, + StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{ + Database: dbM, + Pubsub: ps, + StatsBatcher: batcher, + TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore), + }), + AgentStatsRefreshInterval: 10 * time.Second, + TimeNowFn: func() time.Time { + return now + }, + Experiments: []codersdk.Experiment{codersdk.ExperimentWorkspaceUsage}, + } + + // Workspace gets fetched. + dbM.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(database.GetWorkspaceByAgentIDRow{ + Workspace: workspace, + TemplateName: template.Name, + }, nil) + + // We expect an activity bump because ConnectionCount > 0. + dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{ + WorkspaceID: workspace.ID, + NextAutostart: time.Time{}.UTC(), + }).Return(nil) + + // Workspace last used at gets bumped. + dbM.EXPECT().UpdateWorkspaceLastUsedAt(gomock.Any(), database.UpdateWorkspaceLastUsedAtParams{ + ID: workspace.ID, + LastUsedAt: now, + }).Return(nil) + + resp, err := api.UpdateStats(context.Background(), req) + require.NoError(t, err) + require.Equal(t, &agentproto.UpdateStatsResponse{ + ReportInterval: durationpb.New(10 * time.Second), + }, resp) + + batcher.mu.Lock() + defer batcher.mu.Unlock() + require.EqualValues(t, int64(1), batcher.called) + require.EqualValues(t, batcher.lastStats.SessionCountVscode, 0) + require.EqualValues(t, batcher.lastStats.SessionCountJetbrains, 0) + require.EqualValues(t, batcher.lastStats.SessionCountReconnectingPty, 0) + require.EqualValues(t, batcher.lastStats.SessionCountSsh, 0) + }) } func templateScheduleStorePtr(store schedule.TemplateScheduleStore) *atomic.Pointer[schedule.TemplateScheduleStore] { From 64defe814af36ddc0fd2315434482dfd0425e9c7 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 14 Jun 2024 19:55:54 +0000 Subject: [PATCH 02/14] connect experiments to coderd --- coderd/agentapi/api.go | 3 +++ coderd/workspaceagentsrpc.go | 1 + 2 files changed, 4 insertions(+) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index ae0d594314e66..ef47d70cf7f8f 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -25,6 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" @@ -64,6 +65,7 @@ type Options struct { AppearanceFetcher *atomic.Pointer[appearance.Fetcher] PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID) PublishWorkspaceAgentLogsUpdateFn func(ctx context.Context, workspaceAgentID uuid.UUID, msg agentsdk.LogsNotifyMessage) + Experiments codersdk.Experiments AccessURL *url.URL AppHostname string @@ -118,6 +120,7 @@ func New(opts Options) *API { Log: opts.Log, StatsReporter: opts.StatsReporter, AgentStatsRefreshInterval: opts.AgentStatsRefreshInterval, + Experiments: opts.Experiments, } api.LifecycleAPI = &LifecycleAPI{ diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index ec8dcd8a0e3fc..bd235ee37d741 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -135,6 +135,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { StatsReporter: api.statsReporter, PublishWorkspaceUpdateFn: api.publishWorkspaceUpdate, PublishWorkspaceAgentLogsUpdateFn: api.publishWorkspaceAgentLogsUpdate, + Experiments: api.Experiments, AccessURL: api.AccessURL, AppHostname: api.AppHostname, From 1ab57a4c1c9a4110db34dec36361b1f867e64c5e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 14 Jun 2024 20:02:03 +0000 Subject: [PATCH 03/14] limit to just ssh for now --- coderd/agentapi/stats.go | 9 +++++---- coderd/agentapi/stats_test.go | 7 ++++--- coderd/workspaces.go | 13 +++++++------ codersdk/workspaces.go | 7 ++++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/coderd/agentapi/stats.go b/coderd/agentapi/stats.go index 9998fa9275534..4944c7c96c223 100644 --- a/coderd/agentapi/stats.go +++ b/coderd/agentapi/stats.go @@ -58,13 +58,14 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR ) if a.Experiments.Enabled(codersdk.ExperimentWorkspaceUsage) { - // Session agent stats are being handled by postWorkspaceUsage route when this + // Certain session agent stats are being handled by postWorkspaceUsage route when this // experiment is enabled. We still want most of the stats data but will zero // out the ones being written elsewhere. - req.Stats.SessionCountVscode = 0 - req.Stats.SessionCountJetbrains = 0 - req.Stats.SessionCountReconnectingPty = 0 req.Stats.SessionCountSsh = 0 + // TODO: More session types will be enabled as we migrate over. + // req.Stats.SessionCountVscode = 0 + // req.Stats.SessionCountJetbrains = 0 + // req.Stats.SessionCountReconnectingPty = 0 } err = a.StatsReporter.ReportAgentStats( diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index 53560a2d751d5..a16325d8b2f1a 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -499,10 +499,11 @@ func TestUpdateStates(t *testing.T) { batcher.mu.Lock() defer batcher.mu.Unlock() require.EqualValues(t, int64(1), batcher.called) - require.EqualValues(t, batcher.lastStats.SessionCountVscode, 0) - require.EqualValues(t, batcher.lastStats.SessionCountJetbrains, 0) - require.EqualValues(t, batcher.lastStats.SessionCountReconnectingPty, 0) require.EqualValues(t, batcher.lastStats.SessionCountSsh, 0) + // TODO: other session values will come as they are migrated over + // require.EqualValues(t, batcher.lastStats.SessionCountVscode, 0) + // require.EqualValues(t, batcher.lastStats.SessionCountJetbrains, 0) + // require.EqualValues(t, batcher.lastStats.SessionCountReconnectingPty, 0) }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 22a269fc5fb7f..35c7c079b5262 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1179,14 +1179,15 @@ func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) { ConnectionCount: 1, } switch req.AppName { - case codersdk.UsageAppNameVscode: - stat.SessionCountVscode = 1 - case codersdk.UsageAppNameJetbrains: - stat.SessionCountJetbrains = 1 - case codersdk.UsageAppNameReconnectingPty: - stat.SessionCountReconnectingPty = 1 case codersdk.UsageAppNameSSH: stat.SessionCountSsh = 1 + // TODO: More session types will be enabled as we migrate over. + // case codersdk.UsageAppNameVscode: + // stat.SessionCountVscode = 1 + // case codersdk.UsageAppNameJetbrains: + // stat.SessionCountJetbrains = 1 + // case codersdk.UsageAppNameReconnectingPty: + // stat.SessionCountReconnectingPty = 1 default: // This means the app_name is in the codersdk.AllowedAppNames but not being // handled by this switch statement. diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 69472f8d4579d..d8569a6f33c57 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -331,10 +331,11 @@ const ( ) var AllowedAppNames = []UsageAppName{ - UsageAppNameVscode, - UsageAppNameJetbrains, - UsageAppNameReconnectingPty, UsageAppNameSSH, + // TODO: More session types will be enabled as we migrate over. + // UsageAppNameVscode, + // UsageAppNameJetbrains, + // UsageAppNameReconnectingPty, } // PostWorkspaceUsage marks the workspace as having been used recently and records an app stat. From 3248673eded4af9d9aa97afb409c00bba7606557 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 17:48:56 +0000 Subject: [PATCH 04/14] add ssh and tests --- cli/ssh.go | 7 ++ cli/ssh_test.go | 85 +++++++++++++++++++++++++ coderd/agentapi/stats_test.go | 63 ++++++------------ coderd/coderd.go | 2 +- coderd/coderdtest/coderdtest.go | 2 +- coderd/workspacestats/wstest/batcher.go | 38 +++++++++++ 6 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 coderd/workspacestats/wstest/batcher.go diff --git a/cli/ssh.go b/cli/ssh.go index ac849649b9184..a55cd437ffe3d 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -408,6 +408,13 @@ func (r *RootCmd) ssh() *serpent.Command { return xerrors.Errorf("start shell: %w", err) } + // track workspace usage while connection is open + closeUsage := client.UpdateWorkspaceUsageWithBodyContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{ + AgentID: workspaceAgent.ID, + AppName: codersdk.UsageAppNameSSH, + }) + defer closeUsage() + // Put cancel at the top of the defer stack to initiate // shutdown of services. defer cancel() diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 8c3c1a4e40fd1..c267b991c7ba4 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -36,6 +36,7 @@ import ( "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/agent/agentssh" "github.com/coder/coder/v2/agent/agenttest" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/coderd/coderdtest" @@ -43,6 +44,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/workspacestats/wstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" @@ -1292,6 +1294,89 @@ func TestSSH(t *testing.T) { require.NoError(t, err) require.Len(t, ents, 1, "expected one file in logdir %s", logDir) }) + t.Run("UpdateUsageNoExperiment", func(t *testing.T) { + t.Parallel() + + batcher := &wstest.StatsBatcher{ + LastStats: &agentproto.Stats{}, + } + admin, store := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + StatsBatcher: batcher, + }) + admin.SetLogger(slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug)) + first := coderdtest.CreateFirstUser(t, admin) + client, user := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID) + r := dbfake.WorkspaceBuild(t, store, database.Workspace{ + OrganizationID: first.OrganizationID, + OwnerID: user.ID, + }).WithAgent().Do() + workspace := r.Workspace + agentToken := r.AgentToken + inv, root := clitest.New(t, "ssh", workspace.Name) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) + pty.ExpectMatch("Waiting") + + _ = agenttest.New(t, client.URL, agentToken) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + // Shells on Mac, Windows, and Linux all exit shells with the "exit" command. + pty.WriteLine("exit") + <-cmdDone + + require.EqualValues(t, 0, batcher.Called) + require.EqualValues(t, 0, batcher.LastStats.SessionCountSsh) + }) + t.Run("UpdateUsageExperiment", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)} + batcher := &wstest.StatsBatcher{} + admin, store := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + StatsBatcher: batcher, + }) + admin.SetLogger(slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug)) + first := coderdtest.CreateFirstUser(t, admin) + client, user := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID) + r := dbfake.WorkspaceBuild(t, store, database.Workspace{ + OrganizationID: first.OrganizationID, + OwnerID: user.ID, + }).WithAgent().Do() + workspace := r.Workspace + agentToken := r.AgentToken + inv, root := clitest.New(t, "ssh", workspace.Name) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) + pty.ExpectMatch("Waiting") + + _ = agenttest.New(t, client.URL, agentToken) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + // Shells on Mac, Windows, and Linux all exit shells with the "exit" command. + pty.WriteLine("exit") + <-cmdDone + + require.EqualValues(t, 1, batcher.Called) + require.EqualValues(t, 1, batcher.LastStats.SessionCountSsh) + }) } //nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel. diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index a16325d8b2f1a..5ca342ef9b54b 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -3,7 +3,6 @@ package agentapi_test import ( "context" "database/sql" - "sync" "sync/atomic" "testing" "time" @@ -23,37 +22,11 @@ import ( "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/workspacestats" + "github.com/coder/coder/v2/coderd/workspacestats/wstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) -type statsBatcher struct { - mu sync.Mutex - - called int64 - lastTime time.Time - lastAgentID uuid.UUID - lastTemplateID uuid.UUID - lastUserID uuid.UUID - lastWorkspaceID uuid.UUID - lastStats *agentproto.Stats -} - -var _ workspacestats.Batcher = &statsBatcher{} - -func (b *statsBatcher) Add(now time.Time, agentID uuid.UUID, templateID uuid.UUID, userID uuid.UUID, workspaceID uuid.UUID, st *agentproto.Stats) error { - b.mu.Lock() - defer b.mu.Unlock() - b.called++ - b.lastTime = now - b.lastAgentID = agentID - b.lastTemplateID = templateID - b.lastUserID = userID - b.lastWorkspaceID = workspaceID - b.lastStats = st - return nil -} - func TestUpdateStates(t *testing.T) { t.Parallel() @@ -94,7 +67,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &statsBatcher{} + batcher = &wstest.StatsBatcher{} updateAgentMetricsFnCalled = false req = &agentproto.UpdateStatsRequest{ @@ -188,15 +161,15 @@ func TestUpdateStates(t *testing.T) { ReportInterval: durationpb.New(10 * time.Second), }, resp) - batcher.mu.Lock() - defer batcher.mu.Unlock() - require.Equal(t, int64(1), batcher.called) - require.Equal(t, now, batcher.lastTime) - require.Equal(t, agent.ID, batcher.lastAgentID) - require.Equal(t, template.ID, batcher.lastTemplateID) - require.Equal(t, user.ID, batcher.lastUserID) - require.Equal(t, workspace.ID, batcher.lastWorkspaceID) - require.Equal(t, req.Stats, batcher.lastStats) + batcher.Mu.Lock() + defer batcher.Mu.Unlock() + require.Equal(t, int64(1), batcher.Called) + require.Equal(t, now, batcher.LastTime) + require.Equal(t, agent.ID, batcher.LastAgentID) + require.Equal(t, template.ID, batcher.LastTemplateID) + require.Equal(t, user.ID, batcher.LastUserID) + require.Equal(t, workspace.ID, batcher.LastWorkspaceID) + require.Equal(t, req.Stats, batcher.LastStats) ctx := testutil.Context(t, testutil.WaitShort) select { case <-ctx.Done(): @@ -222,7 +195,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &statsBatcher{} + batcher = &wstest.StatsBatcher{} req = &agentproto.UpdateStatsRequest{ Stats: &agentproto.Stats{ @@ -336,7 +309,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &statsBatcher{} + batcher = &wstest.StatsBatcher{} updateAgentMetricsFnCalled = false req = &agentproto.UpdateStatsRequest{ @@ -423,7 +396,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &statsBatcher{} + batcher = &wstest.StatsBatcher{} req = &agentproto.UpdateStatsRequest{ Stats: &agentproto.Stats{ @@ -496,10 +469,10 @@ func TestUpdateStates(t *testing.T) { ReportInterval: durationpb.New(10 * time.Second), }, resp) - batcher.mu.Lock() - defer batcher.mu.Unlock() - require.EqualValues(t, int64(1), batcher.called) - require.EqualValues(t, batcher.lastStats.SessionCountSsh, 0) + batcher.Mu.Lock() + defer batcher.Mu.Unlock() + require.EqualValues(t, 1, batcher.Called) + require.EqualValues(t, 0, batcher.LastStats.SessionCountSsh) // TODO: other session values will come as they are migrated over // require.EqualValues(t, batcher.lastStats.SessionCountVscode, 0) // require.EqualValues(t, batcher.lastStats.SessionCountJetbrains, 0) diff --git a/coderd/coderd.go b/coderd/coderd.go index e8a698de0de34..bb6478ddb8add 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -187,7 +187,7 @@ type Options struct { HTTPClient *http.Client UpdateAgentMetrics func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) - StatsBatcher *workspacestats.DBBatcher + StatsBatcher workspacestats.Batcher WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 49388aa3537a5..fbeaed43bd8be 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -145,7 +145,7 @@ type Options struct { // Logger should only be overridden if you expect errors // as part of your test. Logger *slog.Logger - StatsBatcher *workspacestats.DBBatcher + StatsBatcher workspacestats.Batcher WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions AllowWorkspaceRenames bool diff --git a/coderd/workspacestats/wstest/batcher.go b/coderd/workspacestats/wstest/batcher.go new file mode 100644 index 0000000000000..23cb99bf47667 --- /dev/null +++ b/coderd/workspacestats/wstest/batcher.go @@ -0,0 +1,38 @@ +package wstest + +import ( + "sync" + "time" + + "github.com/google/uuid" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/workspacestats" +) + +type StatsBatcher struct { + Mu sync.Mutex + + Called int64 + LastTime time.Time + LastAgentID uuid.UUID + LastTemplateID uuid.UUID + LastUserID uuid.UUID + LastWorkspaceID uuid.UUID + LastStats *agentproto.Stats +} + +var _ workspacestats.Batcher = &StatsBatcher{} + +func (b *StatsBatcher) Add(now time.Time, agentID uuid.UUID, templateID uuid.UUID, userID uuid.UUID, workspaceID uuid.UUID, st *agentproto.Stats) error { + b.Mu.Lock() + defer b.Mu.Unlock() + b.Called++ + b.LastTime = now + b.LastAgentID = agentID + b.LastTemplateID = templateID + b.LastUserID = userID + b.LastWorkspaceID = workspaceID + b.LastStats = st + return nil +} From f90bed23f1361a6fce8f41cf5b8abdda0472f684 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:01:36 +0000 Subject: [PATCH 05/14] stop stats reporting at agent level --- agent/agent.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ agent/stats.go | 20 +++++++++++++++++--- cli/agent.go | 3 +++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 5512f04db28ea..576b05f66e3cf 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -86,6 +86,8 @@ type Options struct { PrometheusRegistry *prometheus.Registry ReportMetadataInterval time.Duration ServiceBannerRefreshInterval time.Duration + ExperimentRefreshInterval time.Duration + FetchExperiments func(ctx context.Context) (codersdk.Experiments, error) Syscaller agentproc.Syscaller // ModifiedProcesses is used for testing process priority management. ModifiedProcesses chan []*agentproc.Process @@ -134,6 +136,14 @@ func New(options Options) Agent { return "", nil } } + if options.FetchExperiments == nil { + options.FetchExperiments = func(ctx context.Context) (codersdk.Experiments, error) { + return codersdk.Experiments{}, nil + } + } + if options.ExperimentRefreshInterval == 0 { + options.ExperimentRefreshInterval = 5 * time.Minute + } if options.ReportMetadataInterval == 0 { options.ReportMetadataInterval = time.Second } @@ -167,6 +177,7 @@ func New(options Options) Agent { environmentVariables: options.EnvironmentVariables, client: options.Client, exchangeToken: options.ExchangeToken, + fetchExperiments: options.FetchExperiments, filesystem: options.Filesystem, logDir: options.LogDir, tempDir: options.TempDir, @@ -249,6 +260,10 @@ type agent struct { lifecycleStates []agentsdk.PostLifecycleRequest lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported. + fetchExperiments func(ctx context.Context) (codersdk.Experiments, error) + fetchExperimentsInterval time.Duration + experiments atomic.Pointer[codersdk.Experiments] + network *tailnet.Conn addresses []netip.Prefix statsReporter *statsReporter @@ -737,6 +752,28 @@ func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) erro } } +// fetchExperimentsLoop fetches experiments on an interval. +func (a *agent) fetchExperimentsLoop(ctx context.Context) error { + ticker := time.NewTicker(a.fetchExperimentsInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + experiments, err := a.fetchExperiments(ctx) + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + a.logger.Error(ctx, "failed to update experiments", slog.Error(err)) + return err + } + a.experiments.Store(&experiments) + } + } +} + func (a *agent) run() (retErr error) { // This allows the agent to refresh it's token if necessary. // For instance identity this is required, since the instance @@ -747,6 +784,12 @@ func (a *agent) run() (retErr error) { } a.sessionToken.Store(&sessionToken) + exp, err := a.fetchExperiments(a.hardCtx) + if err != nil { + return xerrors.Errorf("fetch experiments: %w", err) + } + a.experiments.Store(&exp) + // ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs conn, err := a.client.ConnectRPC(a.hardCtx) if err != nil { @@ -856,6 +899,10 @@ func (a *agent) run() (retErr error) { connMan.start("fetch service banner loop", gracefulShutdownBehaviorStop, a.fetchServiceBannerLoop) + connMan.start("fetch experiments loop", gracefulShutdownBehaviorStop, func(ctx context.Context, _ drpc.Conn) error { + return a.fetchExperimentsLoop(ctx) + }) + connMan.start("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, conn drpc.Conn) error { if err := networkOK.wait(ctx); err != nil { return xerrors.Errorf("no network: %w", err) diff --git a/agent/stats.go b/agent/stats.go index 2615ab339637b..6855e55df0ee4 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -5,11 +5,13 @@ import ( "sync" "time" + "go.uber.org/atomic" "golang.org/x/xerrors" "tailscale.com/types/netlogtype" "cdr.dev/slog" "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/codersdk" ) const maxConns = 2048 @@ -36,9 +38,10 @@ type statsReporter struct { unreported bool lastInterval time.Duration - source networkStatsSource - collector statsCollector - logger slog.Logger + source networkStatsSource + collector statsCollector + logger slog.Logger + experiments atomic.Pointer[codersdk.Experiments] } func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector) *statsReporter { @@ -112,6 +115,17 @@ func (s *statsReporter) reportLocked( s.L.Unlock() defer s.L.Lock() stats := s.collector.Collect(ctx, networkStats) + + // if the experiment is enabled we zero out certain session stats + // as we migrate to the client reporting these stats instead. + if s.experiments.Load().Enabled(codersdk.ExperimentWorkspaceUsage) { + stats.SessionCountSsh = 0 + // TODO: More session types will be enabled as we migrate over. + // stats.SessionCountVscode = 0 + // stats.SessionCountJetbrains = 0 + // stats.SessionCountReconnectingPty = 0 + } + resp, err := dest.UpdateStats(ctx, &proto.UpdateStatsRequest{Stats: stats}) if err != nil { return err diff --git a/cli/agent.go b/cli/agent.go index 5465aeedd9302..344c042a2e2fa 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -306,6 +306,9 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { client.SetSessionToken(resp.SessionToken) return resp.SessionToken, nil }, + FetchExperiments: func(ctx context.Context) (codersdk.Experiments, error) { + return client.SDK.Experiments(ctx) + }, EnvironmentVariables: environmentVariables, IgnorePorts: ignorePorts, SSHMaxTimeout: sshMaxTimeout, From 52d231c12ec0783f7289919112e37384e21637b6 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:03:19 +0000 Subject: [PATCH 06/14] remove test --- coderd/agentapi/api.go | 1 - coderd/agentapi/stats.go | 13 ----- coderd/agentapi/stats_test.go | 99 ----------------------------------- 3 files changed, 113 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index ef47d70cf7f8f..dbea11931c5e8 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -120,7 +120,6 @@ func New(opts Options) *API { Log: opts.Log, StatsReporter: opts.StatsReporter, AgentStatsRefreshInterval: opts.AgentStatsRefreshInterval, - Experiments: opts.Experiments, } api.LifecycleAPI = &LifecycleAPI{ diff --git a/coderd/agentapi/stats.go b/coderd/agentapi/stats.go index 4944c7c96c223..a167fb5d6f275 100644 --- a/coderd/agentapi/stats.go +++ b/coderd/agentapi/stats.go @@ -12,7 +12,6 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/workspacestats" - "github.com/coder/coder/v2/codersdk" ) type StatsAPI struct { @@ -21,7 +20,6 @@ type StatsAPI struct { Log slog.Logger StatsReporter *workspacestats.Reporter AgentStatsRefreshInterval time.Duration - Experiments codersdk.Experiments TimeNowFn func() time.Time // defaults to dbtime.Now() } @@ -57,17 +55,6 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR slog.F("payload", req), ) - if a.Experiments.Enabled(codersdk.ExperimentWorkspaceUsage) { - // Certain session agent stats are being handled by postWorkspaceUsage route when this - // experiment is enabled. We still want most of the stats data but will zero - // out the ones being written elsewhere. - req.Stats.SessionCountSsh = 0 - // TODO: More session types will be enabled as we migrate over. - // req.Stats.SessionCountVscode = 0 - // req.Stats.SessionCountJetbrains = 0 - // req.Stats.SessionCountReconnectingPty = 0 - } - err = a.StatsReporter.ReportAgentStats( ctx, a.now(), diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index 5ca342ef9b54b..c69ad652fecb6 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -379,105 +379,6 @@ func TestUpdateStates(t *testing.T) { require.True(t, updateAgentMetricsFnCalled) }) - - t.Run("WorkspaceUsageExperiment", func(t *testing.T) { - t.Parallel() - - var ( - now = dbtime.Now() - dbM = dbmock.NewMockStore(gomock.NewController(t)) - ps = pubsub.NewInMemory() - - templateScheduleStore = schedule.MockTemplateScheduleStore{ - GetFn: func(context.Context, database.Store, uuid.UUID) (schedule.TemplateScheduleOptions, error) { - panic("should not be called") - }, - SetFn: func(context.Context, database.Store, database.Template, schedule.TemplateScheduleOptions) (database.Template, error) { - panic("not implemented") - }, - } - batcher = &wstest.StatsBatcher{} - - req = &agentproto.UpdateStatsRequest{ - Stats: &agentproto.Stats{ - ConnectionsByProto: map[string]int64{ - "tcp": 1, - "dean": 2, - }, - ConnectionCount: 3, - ConnectionMedianLatencyMs: 23, - RxPackets: 120, - RxBytes: 1000, - TxPackets: 130, - TxBytes: 2000, - SessionCountVscode: 1, - SessionCountJetbrains: 2, - SessionCountReconnectingPty: 3, - SessionCountSsh: 4, - Metrics: []*agentproto.Stats_Metric{ - { - Name: "awesome metric", - Value: 42, - }, - { - Name: "uncool metric", - Value: 0, - }, - }, - }, - } - ) - api := agentapi.StatsAPI{ - AgentFn: func(context.Context) (database.WorkspaceAgent, error) { - return agent, nil - }, - Database: dbM, - StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{ - Database: dbM, - Pubsub: ps, - StatsBatcher: batcher, - TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore), - }), - AgentStatsRefreshInterval: 10 * time.Second, - TimeNowFn: func() time.Time { - return now - }, - Experiments: []codersdk.Experiment{codersdk.ExperimentWorkspaceUsage}, - } - - // Workspace gets fetched. - dbM.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(database.GetWorkspaceByAgentIDRow{ - Workspace: workspace, - TemplateName: template.Name, - }, nil) - - // We expect an activity bump because ConnectionCount > 0. - dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{ - WorkspaceID: workspace.ID, - NextAutostart: time.Time{}.UTC(), - }).Return(nil) - - // Workspace last used at gets bumped. - dbM.EXPECT().UpdateWorkspaceLastUsedAt(gomock.Any(), database.UpdateWorkspaceLastUsedAtParams{ - ID: workspace.ID, - LastUsedAt: now, - }).Return(nil) - - resp, err := api.UpdateStats(context.Background(), req) - require.NoError(t, err) - require.Equal(t, &agentproto.UpdateStatsResponse{ - ReportInterval: durationpb.New(10 * time.Second), - }, resp) - - batcher.Mu.Lock() - defer batcher.Mu.Unlock() - require.EqualValues(t, 1, batcher.Called) - require.EqualValues(t, 0, batcher.LastStats.SessionCountSsh) - // TODO: other session values will come as they are migrated over - // require.EqualValues(t, batcher.lastStats.SessionCountVscode, 0) - // require.EqualValues(t, batcher.lastStats.SessionCountJetbrains, 0) - // require.EqualValues(t, batcher.lastStats.SessionCountReconnectingPty, 0) - }) } func templateScheduleStorePtr(store schedule.TemplateScheduleStore) *atomic.Pointer[schedule.TemplateScheduleStore] { From d19a4da1ed619c97ee1b9c924ad6f66d570a6ca3 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:11:36 +0000 Subject: [PATCH 07/14] connect --- agent/agent.go | 2 +- agent/stats.go | 13 +++++++------ agent/stats_internal_test.go | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 576b05f66e3cf..3464c33c746af 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1052,7 +1052,7 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co closed := a.isClosed() if !closed { a.network = network - a.statsReporter = newStatsReporter(a.logger, network, a) + a.statsReporter = newStatsReporter(a.logger, network, a, &a.experiments) } a.closeMutex.Unlock() if closed { diff --git a/agent/stats.go b/agent/stats.go index 6855e55df0ee4..d742b7fa55256 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -41,15 +41,16 @@ type statsReporter struct { source networkStatsSource collector statsCollector logger slog.Logger - experiments atomic.Pointer[codersdk.Experiments] + experiments *atomic.Pointer[codersdk.Experiments] } -func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector) *statsReporter { +func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector, experiments *atomic.Pointer[codersdk.Experiments]) *statsReporter { return &statsReporter{ - Cond: sync.NewCond(&sync.Mutex{}), - logger: logger, - source: source, - collector: collector, + Cond: sync.NewCond(&sync.Mutex{}), + logger: logger, + source: source, + collector: collector, + experiments: experiments, } } diff --git a/agent/stats_internal_test.go b/agent/stats_internal_test.go index 57b21a655a493..aad5158abd7c3 100644 --- a/agent/stats_internal_test.go +++ b/agent/stats_internal_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/atomic" "google.golang.org/protobuf/types/known/durationpb" "tailscale.com/types/ipproto" @@ -20,6 +21,7 @@ import ( "cdr.dev/slog/sloggers/slogjson" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -30,7 +32,7 @@ func TestStatsReporter(t *testing.T) { fSource := newFakeNetworkStatsSource(ctx, t) fCollector := newFakeCollector(t) fDest := newFakeStatsDest() - uut := newStatsReporter(logger, fSource, fCollector) + uut := newStatsReporter(logger, fSource, fCollector, &atomic.Pointer[codersdk.Experiments]{}) loopErr := make(chan error, 1) loopCtx, loopCancel := context.WithCancel(ctx) From eb09c6f205e402151282a2a9a5737479cd2fad28 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:12:22 +0000 Subject: [PATCH 08/14] add interval --- agent/agent.go | 1 + 1 file changed, 1 insertion(+) diff --git a/agent/agent.go b/agent/agent.go index 3464c33c746af..bd12a9992fd46 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -178,6 +178,7 @@ func New(options Options) Agent { client: options.Client, exchangeToken: options.ExchangeToken, fetchExperiments: options.FetchExperiments, + fetchExperimentsInterval: options.ExperimentRefreshInterval, filesystem: options.Filesystem, logDir: options.LogDir, tempDir: options.TempDir, From fefc2deba4232fb65fad0a6b7145b6766ac7bca0 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:21:13 +0000 Subject: [PATCH 09/14] remove loop --- agent/agent.go | 41 ++++-------------------------------- agent/stats.go | 7 +++--- agent/stats_internal_test.go | 3 +-- 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index bd12a9992fd46..f48b9281439f5 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -86,7 +86,6 @@ type Options struct { PrometheusRegistry *prometheus.Registry ReportMetadataInterval time.Duration ServiceBannerRefreshInterval time.Duration - ExperimentRefreshInterval time.Duration FetchExperiments func(ctx context.Context) (codersdk.Experiments, error) Syscaller agentproc.Syscaller // ModifiedProcesses is used for testing process priority management. @@ -141,9 +140,6 @@ func New(options Options) Agent { return codersdk.Experiments{}, nil } } - if options.ExperimentRefreshInterval == 0 { - options.ExperimentRefreshInterval = 5 * time.Minute - } if options.ReportMetadataInterval == 0 { options.ReportMetadataInterval = time.Second } @@ -178,7 +174,6 @@ func New(options Options) Agent { client: options.Client, exchangeToken: options.ExchangeToken, fetchExperiments: options.FetchExperiments, - fetchExperimentsInterval: options.ExperimentRefreshInterval, filesystem: options.Filesystem, logDir: options.LogDir, tempDir: options.TempDir, @@ -261,9 +256,8 @@ type agent struct { lifecycleStates []agentsdk.PostLifecycleRequest lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported. - fetchExperiments func(ctx context.Context) (codersdk.Experiments, error) - fetchExperimentsInterval time.Duration - experiments atomic.Pointer[codersdk.Experiments] + fetchExperiments func(ctx context.Context) (codersdk.Experiments, error) + experiments codersdk.Experiments network *tailnet.Conn addresses []netip.Prefix @@ -753,28 +747,6 @@ func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) erro } } -// fetchExperimentsLoop fetches experiments on an interval. -func (a *agent) fetchExperimentsLoop(ctx context.Context) error { - ticker := time.NewTicker(a.fetchExperimentsInterval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-ticker.C: - experiments, err := a.fetchExperiments(ctx) - if err != nil { - if ctx.Err() != nil { - return ctx.Err() - } - a.logger.Error(ctx, "failed to update experiments", slog.Error(err)) - return err - } - a.experiments.Store(&experiments) - } - } -} - func (a *agent) run() (retErr error) { // This allows the agent to refresh it's token if necessary. // For instance identity this is required, since the instance @@ -785,11 +757,10 @@ func (a *agent) run() (retErr error) { } a.sessionToken.Store(&sessionToken) - exp, err := a.fetchExperiments(a.hardCtx) + a.experiments, err = a.fetchExperiments(a.hardCtx) if err != nil { return xerrors.Errorf("fetch experiments: %w", err) } - a.experiments.Store(&exp) // ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs conn, err := a.client.ConnectRPC(a.hardCtx) @@ -900,10 +871,6 @@ func (a *agent) run() (retErr error) { connMan.start("fetch service banner loop", gracefulShutdownBehaviorStop, a.fetchServiceBannerLoop) - connMan.start("fetch experiments loop", gracefulShutdownBehaviorStop, func(ctx context.Context, _ drpc.Conn) error { - return a.fetchExperimentsLoop(ctx) - }) - connMan.start("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, conn drpc.Conn) error { if err := networkOK.wait(ctx); err != nil { return xerrors.Errorf("no network: %w", err) @@ -1053,7 +1020,7 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co closed := a.isClosed() if !closed { a.network = network - a.statsReporter = newStatsReporter(a.logger, network, a, &a.experiments) + a.statsReporter = newStatsReporter(a.logger, network, a, a.experiments) } a.closeMutex.Unlock() if closed { diff --git a/agent/stats.go b/agent/stats.go index d742b7fa55256..2268ecf8af21b 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -5,7 +5,6 @@ import ( "sync" "time" - "go.uber.org/atomic" "golang.org/x/xerrors" "tailscale.com/types/netlogtype" @@ -41,10 +40,10 @@ type statsReporter struct { source networkStatsSource collector statsCollector logger slog.Logger - experiments *atomic.Pointer[codersdk.Experiments] + experiments codersdk.Experiments } -func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector, experiments *atomic.Pointer[codersdk.Experiments]) *statsReporter { +func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector, experiments codersdk.Experiments) *statsReporter { return &statsReporter{ Cond: sync.NewCond(&sync.Mutex{}), logger: logger, @@ -119,7 +118,7 @@ func (s *statsReporter) reportLocked( // if the experiment is enabled we zero out certain session stats // as we migrate to the client reporting these stats instead. - if s.experiments.Load().Enabled(codersdk.ExperimentWorkspaceUsage) { + if s.experiments.Enabled(codersdk.ExperimentWorkspaceUsage) { stats.SessionCountSsh = 0 // TODO: More session types will be enabled as we migrate over. // stats.SessionCountVscode = 0 diff --git a/agent/stats_internal_test.go b/agent/stats_internal_test.go index aad5158abd7c3..5ec6c7791873d 100644 --- a/agent/stats_internal_test.go +++ b/agent/stats_internal_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/stretchr/testify/require" - "go.uber.org/atomic" "google.golang.org/protobuf/types/known/durationpb" "tailscale.com/types/ipproto" @@ -32,7 +31,7 @@ func TestStatsReporter(t *testing.T) { fSource := newFakeNetworkStatsSource(ctx, t) fCollector := newFakeCollector(t) fDest := newFakeStatsDest() - uut := newStatsReporter(logger, fSource, fCollector, &atomic.Pointer[codersdk.Experiments]{}) + uut := newStatsReporter(logger, fSource, fCollector, codersdk.Experiments{}) loopErr := make(chan error, 1) loopCtx, loopCancel := context.WithCancel(ctx) From 39b78e943863cda1fd365985bcf9ed5bf69e2bfe Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 17 Jun 2024 19:36:36 +0000 Subject: [PATCH 10/14] disable tests --- coderd/workspaces_test.go | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e5a01df9f8edc..a068d1a4ca5ec 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -3462,24 +3462,6 @@ func TestWorkspaceUsageTracking(t *testing.T) { }) require.ErrorContains(t, err, "app_name") - // vscode works - err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ - AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, - AppName: "vscode", - }) - require.NoError(t, err) - // jetbrains works - err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ - AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, - AppName: "jetbrains", - }) - require.NoError(t, err) - // reconnecting-pty works - err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ - AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, - AppName: "reconnecting-pty", - }) - require.NoError(t, err) // ssh works err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, @@ -3487,6 +3469,26 @@ func TestWorkspaceUsageTracking(t *testing.T) { }) require.NoError(t, err) + // TODO: Enable these tests once each stat is moved over. + // // vscode works + // err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + // AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + // AppName: "vscode", + // }) + // require.NoError(t, err) + // // jetbrains works + // err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + // AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + // AppName: "jetbrains", + // }) + // require.NoError(t, err) + // // reconnecting-pty works + // err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{ + // AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID, + // AppName: "reconnecting-pty", + // }) + // require.NoError(t, err) + // ensure deadline has been bumped newWorkspace, err := client.Workspace(ctx, r.Workspace.ID) require.NoError(t, err) From cf80efe1eeea06580a9f975eb71d04506c47fd5a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 18 Jun 2024 16:49:22 +0000 Subject: [PATCH 11/14] move to drpc --- agent/agent.go | 17 +- agent/agenttest/client.go | 7 + agent/proto/agent.pb.go | 371 ++++++++++++++++++++++----------- agent/proto/agent.proto | 7 + agent/proto/agent_drpc.pb.go | 42 +++- agent/stats.go | 19 +- agent/stats_internal_test.go | 81 ++++++- cli/agent.go | 3 - coderd/agentapi/api.go | 5 + coderd/agentapi/experiments.go | 17 ++ codersdk/agentsdk/convert.go | 18 ++ 11 files changed, 435 insertions(+), 152 deletions(-) create mode 100644 coderd/agentapi/experiments.go diff --git a/agent/agent.go b/agent/agent.go index f48b9281439f5..5512f04db28ea 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -86,7 +86,6 @@ type Options struct { PrometheusRegistry *prometheus.Registry ReportMetadataInterval time.Duration ServiceBannerRefreshInterval time.Duration - FetchExperiments func(ctx context.Context) (codersdk.Experiments, error) Syscaller agentproc.Syscaller // ModifiedProcesses is used for testing process priority management. ModifiedProcesses chan []*agentproc.Process @@ -135,11 +134,6 @@ func New(options Options) Agent { return "", nil } } - if options.FetchExperiments == nil { - options.FetchExperiments = func(ctx context.Context) (codersdk.Experiments, error) { - return codersdk.Experiments{}, nil - } - } if options.ReportMetadataInterval == 0 { options.ReportMetadataInterval = time.Second } @@ -173,7 +167,6 @@ func New(options Options) Agent { environmentVariables: options.EnvironmentVariables, client: options.Client, exchangeToken: options.ExchangeToken, - fetchExperiments: options.FetchExperiments, filesystem: options.Filesystem, logDir: options.LogDir, tempDir: options.TempDir, @@ -256,9 +249,6 @@ type agent struct { lifecycleStates []agentsdk.PostLifecycleRequest lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported. - fetchExperiments func(ctx context.Context) (codersdk.Experiments, error) - experiments codersdk.Experiments - network *tailnet.Conn addresses []netip.Prefix statsReporter *statsReporter @@ -757,11 +747,6 @@ func (a *agent) run() (retErr error) { } a.sessionToken.Store(&sessionToken) - a.experiments, err = a.fetchExperiments(a.hardCtx) - if err != nil { - return xerrors.Errorf("fetch experiments: %w", err) - } - // ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs conn, err := a.client.ConnectRPC(a.hardCtx) if err != nil { @@ -1020,7 +1005,7 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co closed := a.isClosed() if !closed { a.network = network - a.statsReporter = newStatsReporter(a.logger, network, a, a.experiments) + a.statsReporter = newStatsReporter(a.logger, network, a) } a.closeMutex.Unlock() if closed { diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 3a4fa4de60b26..14350a204d023 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -170,6 +170,7 @@ type FakeAgentAPI struct { logsCh chan<- *agentproto.BatchCreateLogsRequest lifecycleStates []codersdk.WorkspaceAgentLifecycle metadata map[string]agentsdk.Metadata + experiments codersdk.Experiments getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error) } @@ -266,6 +267,12 @@ func (f *FakeAgentAPI) BatchUpdateMetadata(ctx context.Context, req *agentproto. return &agentproto.BatchUpdateMetadataResponse{}, nil } +func (f *FakeAgentAPI) GetExperiments(ctx context.Context, req *agentproto.GetExperimentsRequest) (*agentproto.GetExperimentsResponse, error) { + f.Lock() + defer f.Unlock() + return agentsdk.ProtoFromExperiments(f.experiments), nil +} + func (f *FakeAgentAPI) SetLogsChannel(ch chan<- *agentproto.BatchCreateLogsRequest) { f.Lock() defer f.Unlock() diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 35e62ace80ce5..96b0dd89f448d 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -2007,6 +2007,91 @@ func (x *BannerConfig) GetBackgroundColor() string { return "" } +type GetExperimentsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetExperimentsRequest) Reset() { + *x = GetExperimentsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetExperimentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExperimentsRequest) ProtoMessage() {} + +func (x *GetExperimentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExperimentsRequest.ProtoReflect.Descriptor instead. +func (*GetExperimentsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{25} +} + +type GetExperimentsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Experiments []string `protobuf:"bytes,1,rep,name=experiments,proto3" json:"experiments,omitempty"` +} + +func (x *GetExperimentsResponse) Reset() { + *x = GetExperimentsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetExperimentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExperimentsResponse) ProtoMessage() {} + +func (x *GetExperimentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExperimentsResponse.ProtoReflect.Descriptor instead. +func (*GetExperimentsResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{26} +} + +func (x *GetExperimentsResponse) GetExperiments() []string { + if x != nil { + return x.Experiments + } + return nil +} + type WorkspaceApp_Healthcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2020,7 +2105,7 @@ type WorkspaceApp_Healthcheck struct { func (x *WorkspaceApp_Healthcheck) Reset() { *x = WorkspaceApp_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[25] + mi := &file_agent_proto_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2033,7 +2118,7 @@ func (x *WorkspaceApp_Healthcheck) String() string { func (*WorkspaceApp_Healthcheck) ProtoMessage() {} func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[25] + mi := &file_agent_proto_agent_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2084,7 +2169,7 @@ type WorkspaceAgentMetadata_Result struct { func (x *WorkspaceAgentMetadata_Result) Reset() { *x = WorkspaceAgentMetadata_Result{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[26] + mi := &file_agent_proto_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2097,7 +2182,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string { func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[26] + mi := &file_agent_proto_agent_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2156,7 +2241,7 @@ type WorkspaceAgentMetadata_Description struct { func (x *WorkspaceAgentMetadata_Description) Reset() { *x = WorkspaceAgentMetadata_Description{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[27] + mi := &file_agent_proto_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2169,7 +2254,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string { func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[27] + mi := &file_agent_proto_agent_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2234,7 +2319,7 @@ type Stats_Metric struct { func (x *Stats_Metric) Reset() { *x = Stats_Metric{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[30] + mi := &file_agent_proto_agent_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2247,7 +2332,7 @@ func (x *Stats_Metric) String() string { func (*Stats_Metric) ProtoMessage() {} func (x *Stats_Metric) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[30] + mi := &file_agent_proto_agent_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2303,7 +2388,7 @@ type Stats_Metric_Label struct { func (x *Stats_Metric_Label) Reset() { *x = Stats_Metric_Label{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[31] + mi := &file_agent_proto_agent_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2316,7 +2401,7 @@ func (x *Stats_Metric_Label) String() string { func (*Stats_Metric_Label) ProtoMessage() {} func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[31] + mi := &file_agent_proto_agent_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2358,7 +2443,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct { func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { *x = BatchUpdateAppHealthRequest_HealthUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[32] + mi := &file_agent_proto_agent_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2371,7 +2456,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[32] + mi := &file_agent_proto_agent_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2758,71 +2843,83 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, - 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, - 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, - 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, - 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, - 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x06, 0x0a, - 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, - 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, + 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, 0x17, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x45, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0x3a, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x63, 0x0a, 0x09, + 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, + 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, + 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, + 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, + 0x04, 0x32, 0xd0, 0x07, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, + 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, - 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, - 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, + 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, + 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, + 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, - 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, - 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, - 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, - 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, + 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, + 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, + 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, + 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2838,7 +2935,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 33) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_agent_proto_agent_proto_goTypes = []interface{}{ (AppHealth)(0), // 0: coder.agent.v2.AppHealth (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel @@ -2872,52 +2969,54 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (*GetAnnouncementBannersRequest)(nil), // 29: coder.agent.v2.GetAnnouncementBannersRequest (*GetAnnouncementBannersResponse)(nil), // 30: coder.agent.v2.GetAnnouncementBannersResponse (*BannerConfig)(nil), // 31: coder.agent.v2.BannerConfig - (*WorkspaceApp_Healthcheck)(nil), // 32: coder.agent.v2.WorkspaceApp.Healthcheck - (*WorkspaceAgentMetadata_Result)(nil), // 33: coder.agent.v2.WorkspaceAgentMetadata.Result - (*WorkspaceAgentMetadata_Description)(nil), // 34: coder.agent.v2.WorkspaceAgentMetadata.Description - nil, // 35: coder.agent.v2.Manifest.EnvironmentVariablesEntry - nil, // 36: coder.agent.v2.Stats.ConnectionsByProtoEntry - (*Stats_Metric)(nil), // 37: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 38: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*durationpb.Duration)(nil), // 40: google.protobuf.Duration - (*proto.DERPMap)(nil), // 41: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp + (*GetExperimentsRequest)(nil), // 32: coder.agent.v2.GetExperimentsRequest + (*GetExperimentsResponse)(nil), // 33: coder.agent.v2.GetExperimentsResponse + (*WorkspaceApp_Healthcheck)(nil), // 34: coder.agent.v2.WorkspaceApp.Healthcheck + (*WorkspaceAgentMetadata_Result)(nil), // 35: coder.agent.v2.WorkspaceAgentMetadata.Result + (*WorkspaceAgentMetadata_Description)(nil), // 36: coder.agent.v2.WorkspaceAgentMetadata.Description + nil, // 37: coder.agent.v2.Manifest.EnvironmentVariablesEntry + nil, // 38: coder.agent.v2.Stats.ConnectionsByProtoEntry + (*Stats_Metric)(nil), // 39: coder.agent.v2.Stats.Metric + (*Stats_Metric_Label)(nil), // 40: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 41: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*durationpb.Duration)(nil), // 42: google.protobuf.Duration + (*proto.DERPMap)(nil), // 43: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel - 32, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck + 34, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 40, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration - 33, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 34, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 35, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 41, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 42, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 35, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 36, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 37, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry + 43, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap 8, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript 7, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp - 34, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 36, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry - 37, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric + 36, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 38, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry + 39, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric 14, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 40, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 42, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration 4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 42, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 44, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp 17, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 39, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 41, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate 5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem 21, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 33, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 35, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result 23, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 42, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 44, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp 6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level 26, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log 31, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig - 40, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 42, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 40, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 40, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 42, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 44, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 42, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 42, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration 3, // 31: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 38, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 40, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label 0, // 33: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth 11, // 34: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest 13, // 35: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest @@ -2928,17 +3027,19 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 24, // 40: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest 27, // 41: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest 29, // 42: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest - 10, // 43: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 12, // 44: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 16, // 45: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 17, // 46: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 20, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 21, // 48: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 25, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 28, // 50: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 30, // 51: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse - 43, // [43:52] is the sub-list for method output_type - 34, // [34:43] is the sub-list for method input_type + 32, // 43: coder.agent.v2.Agent.GetExperiments:input_type -> coder.agent.v2.GetExperimentsRequest + 10, // 44: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 12, // 45: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 16, // 46: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 17, // 47: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 20, // 48: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 21, // 49: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 25, // 50: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 28, // 51: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 30, // 52: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse + 33, // 53: coder.agent.v2.Agent.GetExperiments:output_type -> coder.agent.v2.GetExperimentsResponse + 44, // [44:54] is the sub-list for method output_type + 34, // [34:44] is the sub-list for method input_type 34, // [34:34] is the sub-list for extension type_name 34, // [34:34] is the sub-list for extension extendee 0, // [0:34] is the sub-list for field type_name @@ -3251,7 +3352,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceApp_Healthcheck); i { + switch v := v.(*GetExperimentsRequest); i { case 0: return &v.state case 1: @@ -3263,7 +3364,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Result); i { + switch v := v.(*GetExperimentsResponse); i { case 0: return &v.state case 1: @@ -3275,6 +3376,30 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceApp_Healthcheck); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*WorkspaceAgentMetadata_Description); i { case 0: return &v.state @@ -3286,7 +3411,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric); i { case 0: return &v.state @@ -3298,7 +3423,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric_Label); i { case 0: return &v.state @@ -3310,7 +3435,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -3329,7 +3454,7 @@ func file_agent_proto_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, NumEnums: 7, - NumMessages: 33, + NumMessages: 35, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index 4548ed8e7f2de..efdd2f105698c 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -263,6 +263,12 @@ message BannerConfig { string background_color = 3; } +message GetExperimentsRequest {} + +message GetExperimentsResponse { + repeated string experiments = 1; +} + service Agent { rpc GetManifest(GetManifestRequest) returns (Manifest); rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner); @@ -273,4 +279,5 @@ service Agent { rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse); rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse); rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse); + rpc GetExperiments(GetExperimentsRequest) returns (GetExperimentsResponse); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index 09b3c972c2ce6..f740509a59e6b 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -47,6 +47,7 @@ type DRPCAgentClient interface { BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) + GetExperiments(ctx context.Context, in *GetExperimentsRequest) (*GetExperimentsResponse, error) } type drpcAgentClient struct { @@ -140,6 +141,15 @@ func (c *drpcAgentClient) GetAnnouncementBanners(ctx context.Context, in *GetAnn return out, nil } +func (c *drpcAgentClient) GetExperiments(ctx context.Context, in *GetExperimentsRequest) (*GetExperimentsResponse, error) { + out := new(GetExperimentsResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetExperiments", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAgentServer interface { GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) @@ -150,6 +160,7 @@ type DRPCAgentServer interface { BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) + GetExperiments(context.Context, *GetExperimentsRequest) (*GetExperimentsResponse, error) } type DRPCAgentUnimplementedServer struct{} @@ -190,9 +201,13 @@ func (s *DRPCAgentUnimplementedServer) GetAnnouncementBanners(context.Context, * return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentUnimplementedServer) GetExperiments(context.Context, *GetExperimentsRequest) (*GetExperimentsResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAgentDescription struct{} -func (DRPCAgentDescription) NumMethods() int { return 9 } +func (DRPCAgentDescription) NumMethods() int { return 10 } func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -277,6 +292,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, in1.(*GetAnnouncementBannersRequest), ) }, DRPCAgentServer.GetAnnouncementBanners, true + case 9: + return "/coder.agent.v2.Agent/GetExperiments", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + GetExperiments( + ctx, + in1.(*GetExperimentsRequest), + ) + }, DRPCAgentServer.GetExperiments, true default: return "", nil, nil, nil, false } @@ -429,3 +453,19 @@ func (x *drpcAgent_GetAnnouncementBannersStream) SendAndClose(m *GetAnnouncement } return x.CloseSend() } + +type DRPCAgent_GetExperimentsStream interface { + drpc.Stream + SendAndClose(*GetExperimentsResponse) error +} + +type drpcAgent_GetExperimentsStream struct { + drpc.Stream +} + +func (x *drpcAgent_GetExperimentsStream) SendAndClose(m *GetExperimentsResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/agent/stats.go b/agent/stats.go index 2268ecf8af21b..5e493368d9fc1 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -11,6 +11,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" ) const maxConns = 2048 @@ -24,6 +25,7 @@ type statsCollector interface { } type statsDest interface { + GetExperiments(ctx context.Context, req *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) UpdateStats(ctx context.Context, req *proto.UpdateStatsRequest) (*proto.UpdateStatsResponse, error) } @@ -43,13 +45,12 @@ type statsReporter struct { experiments codersdk.Experiments } -func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector, experiments codersdk.Experiments) *statsReporter { +func newStatsReporter(logger slog.Logger, source networkStatsSource, collector statsCollector) *statsReporter { return &statsReporter{ - Cond: sync.NewCond(&sync.Mutex{}), - logger: logger, - source: source, - collector: collector, - experiments: experiments, + Cond: sync.NewCond(&sync.Mutex{}), + logger: logger, + source: source, + collector: collector, } } @@ -70,6 +71,12 @@ func (s *statsReporter) callback(_, _ time.Time, virtual, _ map[netlogtype.Conne // this that use it. There is no retry and we fail on the first error since // this will be inside a larger retry loop. func (s *statsReporter) reportLoop(ctx context.Context, dest statsDest) error { + exp, err := dest.GetExperiments(ctx, &proto.GetExperimentsRequest{}) + if err != nil { + return xerrors.Errorf("get experiments: %w", err) + } + s.experiments = agentsdk.ExperimentsFromProto(exp) + // send an initial, blank report to get the interval resp, err := dest.UpdateStats(ctx, &proto.UpdateStatsRequest{}) if err != nil { diff --git a/agent/stats_internal_test.go b/agent/stats_internal_test.go index 5ec6c7791873d..ece2ae29a70ca 100644 --- a/agent/stats_internal_test.go +++ b/agent/stats_internal_test.go @@ -31,7 +31,7 @@ func TestStatsReporter(t *testing.T) { fSource := newFakeNetworkStatsSource(ctx, t) fCollector := newFakeCollector(t) fDest := newFakeStatsDest() - uut := newStatsReporter(logger, fSource, fCollector, codersdk.Experiments{}) + uut := newStatsReporter(logger, fSource, fCollector) loopErr := make(chan error, 1) loopCtx, loopCancel := context.WithCancel(ctx) @@ -129,6 +129,73 @@ func TestStatsReporter(t *testing.T) { require.NoError(t, err) } +func TestStatsExperiment(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + fSource := newFakeNetworkStatsSource(ctx, t) + fCollector := newFakeCollector(t) + fDest := newFakeStatsDest() + fDest.experiments.Experiments = append(fDest.experiments.Experiments, string(codersdk.ExperimentWorkspaceUsage)) + uut := newStatsReporter(logger, fSource, fCollector) + + go func() { + _ = uut.reportLoop(ctx, fDest) + }() + + // initial request to get duration + req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + require.NotNil(t, req) + require.Nil(t, req.Stats) + interval := time.Second * 34 + testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval)}) + + // call to source to set the callback and interval + gotInterval := testutil.RequireRecvCtx(ctx, t, fSource.period) + require.Equal(t, interval, gotInterval) + + // callback returning netstats + netStats := map[netlogtype.Connection]netlogtype.Counts{ + { + Proto: ipproto.TCP, + Src: netip.MustParseAddrPort("192.168.1.33:4887"), + Dst: netip.MustParseAddrPort("192.168.2.99:9999"), + }: { + TxPackets: 22, + TxBytes: 23, + RxPackets: 24, + RxBytes: 25, + }, + } + fSource.callback(time.Now(), time.Now(), netStats, nil) + + // collector called to complete the stats + gotNetStats := testutil.RequireRecvCtx(ctx, t, fCollector.calls) + require.Equal(t, netStats, gotNetStats) + + // complete first collection + stats := &proto.Stats{ + SessionCountSsh: 10, + SessionCountJetbrains: 55, + SessionCountVscode: 20, + SessionCountReconnectingPty: 30, + } + testutil.RequireSendCtx(ctx, t, fCollector.stats, stats) + + // destination called to report the first stats + update := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + require.NotNil(t, update) + // confirm certain session counts are zeroed out when + // experiment is enabled. + require.EqualValues(t, 0, update.Stats.SessionCountSsh) + // confirm others are not zeroed out. These will be + // zeroed out in the future as we migrate to workspace + // usage handling these session stats. + require.EqualValues(t, 55, update.Stats.SessionCountJetbrains) + require.EqualValues(t, 20, update.Stats.SessionCountVscode) + require.EqualValues(t, 30, update.Stats.SessionCountReconnectingPty) +} + type fakeNetworkStatsSource struct { sync.Mutex ctx context.Context @@ -190,8 +257,9 @@ func newFakeCollector(t testing.TB) *fakeCollector { } type fakeStatsDest struct { - reqs chan *proto.UpdateStatsRequest - resps chan *proto.UpdateStatsResponse + reqs chan *proto.UpdateStatsRequest + resps chan *proto.UpdateStatsResponse + experiments *proto.GetExperimentsResponse } func (f *fakeStatsDest) UpdateStats(ctx context.Context, req *proto.UpdateStatsRequest) (*proto.UpdateStatsResponse, error) { @@ -209,10 +277,17 @@ func (f *fakeStatsDest) UpdateStats(ctx context.Context, req *proto.UpdateStatsR } } +func (f *fakeStatsDest) GetExperiments(_ context.Context, _ *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) { + return f.experiments, nil +} + func newFakeStatsDest() *fakeStatsDest { return &fakeStatsDest{ reqs: make(chan *proto.UpdateStatsRequest), resps: make(chan *proto.UpdateStatsResponse), + experiments: &proto.GetExperimentsResponse{ + Experiments: []string{}, + }, } } diff --git a/cli/agent.go b/cli/agent.go index 344c042a2e2fa..5465aeedd9302 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -306,9 +306,6 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { client.SetSessionToken(resp.SessionToken) return resp.SessionToken, nil }, - FetchExperiments: func(ctx context.Context) (codersdk.Experiments, error) { - return client.SDK.Experiments(ctx) - }, EnvironmentVariables: environmentVariables, IgnorePorts: ignorePorts, SSHMaxTimeout: sshMaxTimeout, diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index dbea11931c5e8..7bfcc5122c31d 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -44,6 +44,7 @@ type API struct { *MetadataAPI *LogsAPI *tailnet.DRPCService + *ExperimentAPI mu sync.Mutex cachedWorkspaceID uuid.UUID @@ -159,6 +160,10 @@ func New(opts Options) *API { DerpMapFn: opts.DerpMapFn, } + api.ExperimentAPI = &ExperimentAPI{ + experiments: opts.Experiments, + } + return api } diff --git a/coderd/agentapi/experiments.go b/coderd/agentapi/experiments.go new file mode 100644 index 0000000000000..58f52fef9b09c --- /dev/null +++ b/coderd/agentapi/experiments.go @@ -0,0 +1,17 @@ +package agentapi + +import ( + "context" + + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +type ExperimentAPI struct { + experiments codersdk.Experiments +} + +func (a *ExperimentAPI) GetExperiments(ctx context.Context, _ *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) { + return agentsdk.ProtoFromExperiments(a.experiments), nil +} diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go index fcd2dda414165..d60517a2dd7bc 100644 --- a/codersdk/agentsdk/convert.go +++ b/codersdk/agentsdk/convert.go @@ -379,3 +379,21 @@ func ProtoFromLifecycleState(s codersdk.WorkspaceAgentLifecycle) (proto.Lifecycl } return proto.Lifecycle_State(caps), nil } + +func ProtoFromExperiments(experiments codersdk.Experiments) *proto.GetExperimentsResponse { + exp := make([]string, len(experiments)) + for i, e := range experiments { + exp[i] = string(e) + } + return &proto.GetExperimentsResponse{ + Experiments: exp, + } +} + +func ExperimentsFromProto(resp *proto.GetExperimentsResponse) codersdk.Experiments { + experiments := make(codersdk.Experiments, len(resp.Experiments)) + for i, e := range resp.Experiments { + experiments[i] = codersdk.Experiment(e) + } + return experiments +} From b2fb6af8a84802c8e20286ac3a0a83055931edb3 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 18 Jun 2024 16:58:22 +0000 Subject: [PATCH 12/14] lint --- agent/agenttest/client.go | 2 +- coderd/agentapi/experiments.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 14350a204d023..1315accca7a3b 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -267,7 +267,7 @@ func (f *FakeAgentAPI) BatchUpdateMetadata(ctx context.Context, req *agentproto. return &agentproto.BatchUpdateMetadataResponse{}, nil } -func (f *FakeAgentAPI) GetExperiments(ctx context.Context, req *agentproto.GetExperimentsRequest) (*agentproto.GetExperimentsResponse, error) { +func (f *FakeAgentAPI) GetExperiments(_ context.Context, _ *agentproto.GetExperimentsRequest) (*agentproto.GetExperimentsResponse, error) { f.Lock() defer f.Unlock() return agentsdk.ProtoFromExperiments(f.experiments), nil diff --git a/coderd/agentapi/experiments.go b/coderd/agentapi/experiments.go index 58f52fef9b09c..453ef515f3bcc 100644 --- a/coderd/agentapi/experiments.go +++ b/coderd/agentapi/experiments.go @@ -12,6 +12,6 @@ type ExperimentAPI struct { experiments codersdk.Experiments } -func (a *ExperimentAPI) GetExperiments(ctx context.Context, _ *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) { +func (a *ExperimentAPI) GetExperiments(_ context.Context, _ *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) { return agentsdk.ProtoFromExperiments(a.experiments), nil } From 43a00352d3193f9804e96c056e5b3e7ec45ac8a5 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 20 Jun 2024 18:42:04 +0000 Subject: [PATCH 13/14] pr comments --- agent/stats.go | 10 ++++++---- .../{wstest => workspacestatstest}/batcher.go | 0 tailnet/proto/version.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) rename coderd/workspacestats/{wstest => workspacestatstest}/batcher.go (100%) diff --git a/agent/stats.go b/agent/stats.go index 5e493368d9fc1..ffa786457d4b2 100644 --- a/agent/stats.go +++ b/agent/stats.go @@ -24,7 +24,7 @@ type statsCollector interface { Collect(ctx context.Context, networkStats map[netlogtype.Connection]netlogtype.Counts) *proto.Stats } -type statsDest interface { +type statsAPI interface { GetExperiments(ctx context.Context, req *proto.GetExperimentsRequest) (*proto.GetExperimentsResponse, error) UpdateStats(ctx context.Context, req *proto.UpdateStatsRequest) (*proto.UpdateStatsResponse, error) } @@ -32,7 +32,7 @@ type statsDest interface { // statsReporter is a subcomponent of the agent that handles registering the stats callback on the // networkStatsSource (tailnet.Conn in prod), handling the callback, calling back to the // statsCollector (agent in prod) to collect additional stats, then sending the update to the -// statsDest (agent API in prod) +// statsAPI (agent API in prod) type statsReporter struct { *sync.Cond networkStats *map[netlogtype.Connection]netlogtype.Counts @@ -70,12 +70,14 @@ func (s *statsReporter) callback(_, _ time.Time, virtual, _ map[netlogtype.Conne // connection to the agent API, then passes that connection to go routines like // this that use it. There is no retry and we fail on the first error since // this will be inside a larger retry loop. -func (s *statsReporter) reportLoop(ctx context.Context, dest statsDest) error { +func (s *statsReporter) reportLoop(ctx context.Context, dest statsAPI) error { exp, err := dest.GetExperiments(ctx, &proto.GetExperimentsRequest{}) if err != nil { return xerrors.Errorf("get experiments: %w", err) } + s.L.Lock() s.experiments = agentsdk.ExperimentsFromProto(exp) + s.L.Unlock() // send an initial, blank report to get the interval resp, err := dest.UpdateStats(ctx, &proto.UpdateStatsRequest{}) @@ -115,7 +117,7 @@ func (s *statsReporter) reportLoop(ctx context.Context, dest statsDest) error { } func (s *statsReporter) reportLocked( - ctx context.Context, dest statsDest, networkStats map[netlogtype.Connection]netlogtype.Counts, + ctx context.Context, dest statsAPI, networkStats map[netlogtype.Connection]netlogtype.Counts, ) error { // here we want to do our collecting/reporting while it is unlocked, but then relock // when we return to reportLoop. diff --git a/coderd/workspacestats/wstest/batcher.go b/coderd/workspacestats/workspacestatstest/batcher.go similarity index 100% rename from coderd/workspacestats/wstest/batcher.go rename to coderd/workspacestats/workspacestatstest/batcher.go diff --git a/tailnet/proto/version.go b/tailnet/proto/version.go index 16f324f74fa33..e069b2d2f95f1 100644 --- a/tailnet/proto/version.go +++ b/tailnet/proto/version.go @@ -6,7 +6,7 @@ import ( const ( CurrentMajor = 2 - CurrentMinor = 1 + CurrentMinor = 2 ) var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor).WithBackwardCompat(1) From 8b507318fcf45af9f08f9b563a5bc5adb75468f6 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 20 Jun 2024 18:47:25 +0000 Subject: [PATCH 14/14] rename --- cli/ssh_test.go | 6 +++--- coderd/agentapi/stats_test.go | 8 ++++---- coderd/workspacestats/workspacestatstest/batcher.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/ssh_test.go b/cli/ssh_test.go index c267b991c7ba4..e1d069b612ae7 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -44,7 +44,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/coderd/workspacestats/wstest" + "github.com/coder/coder/v2/coderd/workspacestats/workspacestatstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisionersdk/proto" @@ -1297,7 +1297,7 @@ func TestSSH(t *testing.T) { t.Run("UpdateUsageNoExperiment", func(t *testing.T) { t.Parallel() - batcher := &wstest.StatsBatcher{ + batcher := &workspacestatstest.StatsBatcher{ LastStats: &agentproto.Stats{}, } admin, store := coderdtest.NewWithDatabase(t, &coderdtest.Options{ @@ -1340,7 +1340,7 @@ func TestSSH(t *testing.T) { dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)} - batcher := &wstest.StatsBatcher{} + batcher := &workspacestatstest.StatsBatcher{} admin, store := coderdtest.NewWithDatabase(t, &coderdtest.Options{ DeploymentValues: dv, StatsBatcher: batcher, diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index c69ad652fecb6..d3e71248cd4b2 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -22,7 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/workspacestats" - "github.com/coder/coder/v2/coderd/workspacestats/wstest" + "github.com/coder/coder/v2/coderd/workspacestats/workspacestatstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -67,7 +67,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &wstest.StatsBatcher{} + batcher = &workspacestatstest.StatsBatcher{} updateAgentMetricsFnCalled = false req = &agentproto.UpdateStatsRequest{ @@ -195,7 +195,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &wstest.StatsBatcher{} + batcher = &workspacestatstest.StatsBatcher{} req = &agentproto.UpdateStatsRequest{ Stats: &agentproto.Stats{ @@ -309,7 +309,7 @@ func TestUpdateStates(t *testing.T) { panic("not implemented") }, } - batcher = &wstest.StatsBatcher{} + batcher = &workspacestatstest.StatsBatcher{} updateAgentMetricsFnCalled = false req = &agentproto.UpdateStatsRequest{ diff --git a/coderd/workspacestats/workspacestatstest/batcher.go b/coderd/workspacestats/workspacestatstest/batcher.go index 23cb99bf47667..ad5ba60ad16d0 100644 --- a/coderd/workspacestats/workspacestatstest/batcher.go +++ b/coderd/workspacestats/workspacestatstest/batcher.go @@ -1,4 +1,4 @@ -package wstest +package workspacestatstest import ( "sync"