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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy