feat: add flag to disable template insights (#20940)

Closes #20399 

To summarize the original commit messages:

- Do not log stats to the database.
- Return errors on the insight endpoints.
- Update the frontend to show those errors.
- Also fixes an issue with getting the user status count via codersdk,
since I added a test to ensure it was not disabled by this flag and it
was sending the wrong payload.
This commit is contained in:
Asher
2025-12-14 03:00:03 +00:00
committed by GitHub
parent b9f8295845
commit 27f0413347
19 changed files with 1615 additions and 979 deletions
+130 -1
View File
@@ -28,7 +28,7 @@ import (
"github.com/coder/coder/v2/testutil"
)
func TestUpdateStates(t *testing.T) {
func TestUpdateStats(t *testing.T) {
t.Parallel()
var (
@@ -542,6 +542,135 @@ func TestUpdateStates(t *testing.T) {
}
require.True(t, updateAgentMetricsFnCalled)
})
t.Run("DropStats", 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")
},
}
updateAgentMetricsFnCalled = false
tickCh = make(chan time.Time)
flushCh = make(chan int, 1)
wut = workspacestats.NewTracker(dbM,
workspacestats.TrackerWithTickFlush(tickCh, flushCh),
)
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
},
Workspace: &workspaceAsCacheFields,
Database: dbM,
StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{
Database: dbM,
Pubsub: ps,
StatsBatcher: nil, // Should not be called.
UsageTracker: wut,
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) {
updateAgentMetricsFnCalled = true
assert.Equal(t, prometheusmetrics.AgentMetricLabels{
Username: user.Username,
WorkspaceName: workspace.Name,
AgentName: agent.Name,
TemplateName: template.Name,
}, labels)
assert.Equal(t, req.Stats.Metrics, metrics)
},
DisableDatabaseInserts: true,
}),
AgentStatsRefreshInterval: 10 * time.Second,
TimeNowFn: func() time.Time {
return now
},
}
defer wut.Close()
// 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().BatchUpdateWorkspaceLastUsedAt(gomock.Any(), database.BatchUpdateWorkspaceLastUsedAtParams{
IDs: []uuid.UUID{workspace.ID},
LastUsedAt: now,
}).Return(nil)
// Ensure that pubsub notifications are sent.
notifyDescription := make(chan struct{})
ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
wspubsub.HandleWorkspaceEvent(
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
if err != nil {
return
}
if e.Kind == wspubsub.WorkspaceEventKindStatsUpdate && e.WorkspaceID == workspace.ID {
go func() {
notifyDescription <- struct{}{}
}()
}
}))
resp, err := api.UpdateStats(context.Background(), req)
require.NoError(t, err)
require.Equal(t, &agentproto.UpdateStatsResponse{
ReportInterval: durationpb.New(10 * time.Second),
}, resp)
tickCh <- now
count := <-flushCh
require.Equal(t, 1, count, "expected one flush with one id")
ctx := testutil.Context(t, testutil.WaitShort)
select {
case <-ctx.Done():
t.Error("timed out while waiting for pubsub notification")
case <-notifyDescription:
}
require.True(t, updateAgentMetricsFnCalled)
})
}
func templateScheduleStorePtr(store schedule.TemplateScheduleStore) *atomic.Pointer[schedule.TemplateScheduleStore] {
+11
View File
@@ -14347,6 +14347,9 @@ const docTemplate = `{
"telemetry": {
"$ref": "#/definitions/codersdk.TelemetryConfig"
},
"template_insights": {
"$ref": "#/definitions/codersdk.TemplateInsightsConfig"
},
"terms_of_service_url": {
"type": "string"
},
@@ -18596,6 +18599,14 @@ const docTemplate = `{
}
}
},
"codersdk.TemplateInsightsConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
}
}
},
"codersdk.TemplateInsightsIntervalReport": {
"type": "object",
"properties": {
+11
View File
@@ -12931,6 +12931,9 @@
"telemetry": {
"$ref": "#/definitions/codersdk.TelemetryConfig"
},
"template_insights": {
"$ref": "#/definitions/codersdk.TemplateInsightsConfig"
},
"terms_of_service_url": {
"type": "string"
},
@@ -17032,6 +17035,14 @@
}
}
},
"codersdk.TemplateInsightsConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
}
}
},
"codersdk.TemplateInsightsIntervalReport": {
"type": "object",
"properties": {
+30 -12
View File
@@ -768,14 +768,15 @@ func New(options *Options) *API {
}
api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{
Database: options.Database,
Logger: options.Logger.Named("workspacestats"),
Pubsub: options.Pubsub,
TemplateScheduleStore: options.TemplateScheduleStore,
StatsBatcher: options.StatsBatcher,
UsageTracker: options.WorkspaceUsageTracker,
UpdateAgentMetricsFn: options.UpdateAgentMetrics,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
Database: options.Database,
Logger: options.Logger.Named("workspacestats"),
Pubsub: options.Pubsub,
TemplateScheduleStore: options.TemplateScheduleStore,
StatsBatcher: options.StatsBatcher,
UsageTracker: options.WorkspaceUsageTracker,
UpdateAgentMetricsFn: options.UpdateAgentMetrics,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
DisableDatabaseInserts: !options.DeploymentValues.TemplateInsights.Enable.Value(),
})
workspaceAppsLogger := options.Logger.Named("workspaceapps")
if options.WorkspaceAppsStatsCollectorOptions.Logger == nil {
@@ -1528,11 +1529,28 @@ func New(options *Options) *API {
})
r.Route("/insights", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/daus", api.deploymentDAUs)
r.Get("/user-activity", api.insightsUserActivity)
r.Group(func(r chi.Router) {
r.Use(
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !options.DeploymentValues.TemplateInsights.Enable.Value() {
httpapi.Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{
Message: "Not Found.",
Detail: "Template insights are disabled.",
})
return
}
next.ServeHTTP(rw, r)
})
},
)
r.Get("/daus", api.deploymentDAUs)
r.Get("/user-activity", api.insightsUserActivity)
r.Get("/user-latency", api.insightsUserLatency)
r.Get("/templates", api.insightsTemplates)
})
r.Get("/user-status-counts", api.insightsUserStatusCounts)
r.Get("/user-latency", api.insightsUserLatency)
r.Get("/templates", api.insightsTemplates)
})
r.Route("/debug", func(r chi.Router) {
r.Use(
+185 -48
View File
@@ -520,7 +520,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
return templates, users, testData
}
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) {
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen, disableStorage bool) (*codersdk.Client, chan dbrollup.Event) {
logger := testutil.Logger(t)
db, ps := dbtestutil.NewDB(t)
events := make(chan dbrollup.Event)
@@ -706,22 +706,24 @@ func TestTemplateInsights_Golden(t *testing.T) {
require.NoError(t, err)
defer batcherCloser() // Flushes the stats, this is to ensure they're written.
for workspace, data := range testData {
for _, stat := range data.agentStats {
createdAt := stat.startedAt
connectionCount := int64(1)
if stat.noConnections {
connectionCount = 0
}
for createdAt.Before(stat.endedAt) {
batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
ConnectionCount: connectionCount,
SessionCountVscode: stat.sessionCountVSCode,
SessionCountJetbrains: stat.sessionCountJetBrains,
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
SessionCountSsh: stat.sessionCountSSH,
}, false)
createdAt = createdAt.Add(30 * time.Second)
if !disableStorage {
for workspace, data := range testData {
for _, stat := range data.agentStats {
createdAt := stat.startedAt
connectionCount := int64(1)
if stat.noConnections {
connectionCount = 0
}
for createdAt.Before(stat.endedAt) {
batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
ConnectionCount: connectionCount,
SessionCountVscode: stat.sessionCountVSCode,
SessionCountJetbrains: stat.sessionCountJetBrains,
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
SessionCountSsh: stat.sessionCountSSH,
}, false)
createdAt = createdAt.Add(30 * time.Second)
}
}
}
}
@@ -750,8 +752,9 @@ func TestTemplateInsights_Golden(t *testing.T) {
}
}
reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{
Database: db,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
Database: db,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
DisableDatabaseInserts: disableStorage,
})
err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats)
require.NoError(t, err, "want no error inserting app stats")
@@ -1057,10 +1060,11 @@ func TestTemplateInsights_Golden(t *testing.T) {
ignoreTimes bool
}
tests := []struct {
name string
makeFixture func() ([]*testTemplate, []*testUser)
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
requests []testRequest
name string
makeFixture func() ([]*testTemplate, []*testUser)
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
disableStorage bool
requests []testRequest
}{
{
name: "multiple users and workspaces",
@@ -1237,6 +1241,24 @@ func TestTemplateInsights_Golden(t *testing.T) {
},
},
},
{
name: "disabled",
makeFixture: baseTemplateAndUserFixture,
makeTestData: makeBaseTestData,
disableStorage: true,
requests: []testRequest{
{
name: "week deployment wide",
makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
},
},
},
}
for _, tt := range tests {
@@ -1246,7 +1268,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set")
require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set")
templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData)
client, events := prepare(t, templates, users, testData)
client, events := prepare(t, templates, users, testData, tt.disableStorage)
// Drain two events, the first one resumes rolluper
// operation and the second one waits for the rollup
@@ -1431,7 +1453,7 @@ func TestUserActivityInsights_Golden(t *testing.T) {
return templates, users, testData
}
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) {
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen, disableStorage bool) (*codersdk.Client, chan dbrollup.Event) {
logger := testutil.Logger(t)
db, ps := dbtestutil.NewDB(t)
events := make(chan dbrollup.Event)
@@ -1595,22 +1617,24 @@ func TestUserActivityInsights_Golden(t *testing.T) {
require.NoError(t, err)
defer batcherCloser() // Flushes the stats, this is to ensure they're written.
for workspace, data := range testData {
for _, stat := range data.agentStats {
createdAt := stat.startedAt
connectionCount := int64(1)
if stat.noConnections {
connectionCount = 0
}
for createdAt.Before(stat.endedAt) {
batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
ConnectionCount: connectionCount,
SessionCountVscode: stat.sessionCountVSCode,
SessionCountJetbrains: stat.sessionCountJetBrains,
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
SessionCountSsh: stat.sessionCountSSH,
}, false)
createdAt = createdAt.Add(30 * time.Second)
if !disableStorage {
for workspace, data := range testData {
for _, stat := range data.agentStats {
createdAt := stat.startedAt
connectionCount := int64(1)
if stat.noConnections {
connectionCount = 0
}
for createdAt.Before(stat.endedAt) {
batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{
ConnectionCount: connectionCount,
SessionCountVscode: stat.sessionCountVSCode,
SessionCountJetbrains: stat.sessionCountJetBrains,
SessionCountReconnectingPty: stat.sessionCountReconnectingPTY,
SessionCountSsh: stat.sessionCountSSH,
}, false)
createdAt = createdAt.Add(30 * time.Second)
}
}
}
}
@@ -1639,8 +1663,9 @@ func TestUserActivityInsights_Golden(t *testing.T) {
}
}
reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{
Database: db,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
Database: db,
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
DisableDatabaseInserts: disableStorage,
})
err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats)
require.NoError(t, err, "want no error inserting app stats")
@@ -1902,10 +1927,11 @@ func TestUserActivityInsights_Golden(t *testing.T) {
ignoreTimes bool
}
tests := []struct {
name string
makeFixture func() ([]*testTemplate, []*testUser)
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
requests []testRequest
name string
makeFixture func() ([]*testTemplate, []*testUser)
makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
disableStorage bool
requests []testRequest
}{
{
name: "multiple users and workspaces",
@@ -2013,6 +2039,23 @@ func TestUserActivityInsights_Golden(t *testing.T) {
},
},
},
{
name: "disabled",
makeFixture: baseTemplateAndUserFixture,
makeTestData: makeBaseTestData,
disableStorage: true,
requests: []testRequest{
{
name: "week deployment wide",
makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest {
return codersdk.UserActivityInsightsRequest{
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
}
},
},
},
},
}
for _, tt := range tests {
@@ -2022,7 +2065,7 @@ func TestUserActivityInsights_Golden(t *testing.T) {
require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set")
require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set")
templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData)
client, events := prepare(t, templates, users, testData)
client, events := prepare(t, templates, users, testData, tt.disableStorage)
// Drain two events, the first one resumes rolluper
// operation and the second one waits for the rollup
@@ -2346,3 +2389,97 @@ func TestGenericInsights_RBAC(t *testing.T) {
})
}
}
func TestGenericInsights_Disabled(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
logger := testutil.Logger(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: ps,
Logger: &logger,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
DatabaseRolluper: dbrollup.New(
logger.Named("dbrollup"),
db,
dbrollup.WithInterval(time.Millisecond*100),
),
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
dv.TemplateInsights = codersdk.TemplateInsightsConfig{
Enable: false,
}
}),
})
user := coderdtest.CreateFirstUser(t, client)
_, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
tests := []struct {
name string
fn func(ctx context.Context) error
// ok means there should be no error, otherwise assume 404 due to being
// disabled.
ok bool
}{
{
name: "DAUS",
fn: func(ctx context.Context) error {
_, err := client.DeploymentDAUs(ctx, 0)
return err
},
},
{
name: "UserActivity",
fn: func(ctx context.Context) error {
_, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{})
return err
},
},
{
name: "UserLatency",
fn: func(ctx context.Context) error {
_, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{})
return err
},
},
{
name: "UserStatusCounts",
fn: func(ctx context.Context) error {
_, err := client.GetUserStatusCounts(ctx, codersdk.GetUserStatusCountsRequest{
Offset: 0,
})
return err
},
// Status count is not derived from template insights, so it should not be
// disabled.
ok: true,
},
{
name: "Templates",
fn: func(ctx context.Context) error {
_, err := client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{})
return err
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
err := tt.fn(ctx)
if tt.ok {
require.NoError(t, err)
} else {
require.Error(t, err)
cerr := coderdtest.SDKError(t, err)
require.Contains(t, cerr.Error(), "disabled")
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
}
})
}
}
@@ -0,0 +1,107 @@
{
"report": {
"start_time": "2023-08-15T00:00:00Z",
"end_time": "2023-08-22T00:00:00Z",
"template_ids": [],
"active_users": 0,
"apps_usage": [
{
"template_ids": [],
"type": "builtin",
"display_name": "Visual Studio Code",
"slug": "vscode",
"icon": "/icon/code.svg",
"seconds": 0,
"times_used": 0
},
{
"template_ids": [],
"type": "builtin",
"display_name": "JetBrains",
"slug": "jetbrains",
"icon": "/icon/intellij.svg",
"seconds": 0,
"times_used": 0
},
{
"template_ids": [],
"type": "builtin",
"display_name": "Web Terminal",
"slug": "reconnecting-pty",
"icon": "/icon/terminal.svg",
"seconds": 0,
"times_used": 0
},
{
"template_ids": [],
"type": "builtin",
"display_name": "SSH",
"slug": "ssh",
"icon": "/icon/terminal.svg",
"seconds": 0,
"times_used": 0
},
{
"template_ids": [],
"type": "builtin",
"display_name": "SFTP",
"slug": "sftp",
"icon": "/icon/terminal.svg",
"seconds": 0,
"times_used": 0
}
],
"parameters_usage": []
},
"interval_reports": [
{
"start_time": "2023-08-15T00:00:00Z",
"end_time": "2023-08-16T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-16T00:00:00Z",
"end_time": "2023-08-17T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-17T00:00:00Z",
"end_time": "2023-08-18T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-18T00:00:00Z",
"end_time": "2023-08-19T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-19T00:00:00Z",
"end_time": "2023-08-20T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-20T00:00:00Z",
"end_time": "2023-08-21T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
},
{
"start_time": "2023-08-21T00:00:00Z",
"end_time": "2023-08-22T00:00:00Z",
"template_ids": [],
"interval": "day",
"active_users": 0
}
]
}
@@ -0,0 +1,8 @@
{
"report": {
"start_time": "2023-08-15T00:00:00Z",
"end_time": "2023-08-22T00:00:00Z",
"template_ids": [],
"users": []
}
}
+33 -10
View File
@@ -22,6 +22,23 @@ import (
"github.com/coder/coder/v2/coderd/wspubsub"
)
// TODO: There are currently two paths for reporting activity, both of which are
// tied up with stat collection:
//
// 1. The workspace agent periodically POSTs stats to coderd. On receiving
// this POST, if there is an active SSH or web terminal session, bump both
// the workspace's last_used_at and the deadline.
// 2. The coderd app proxy and wsproxy will periodically report app status
// (coderd calls directly, wsproxy POSTs). This only bumps the workspace's
// last_used_at, as only SSH and web terminal sessions count as activity.
//
// Ideally we would have a single code path for this and we may want to untangle
// activity bumping from stat reporting so we can disable stats collection
// entirely when template insights are disabled rather than having to still
// collect stats but then drop them here.
//
// https://github.com/coder/internal/issues/196
type ReporterOptions struct {
Database database.Store
Logger slog.Logger
@@ -31,6 +48,10 @@ type ReporterOptions struct {
UsageTracker *UsageTracker
UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric)
// DisableDatabaseInserts prevents inserting stats in the database. The
// reporter will still call UpdateAgentMetricsFn and bump workspace activity.
DisableDatabaseInserts bool
AppStatBatchSize int
}
@@ -93,15 +114,12 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta
return nil
}
if err := tx.InsertWorkspaceAppStats(ctx, batch); err != nil {
return err
if !r.opts.DisableDatabaseInserts {
if err := tx.InsertWorkspaceAppStats(ctx, batch); err != nil {
return err
}
}
// TODO: We currently measure workspace usage based on when we get stats from it.
// There are currently two paths for this:
// 1) From SSH -> workspace agent stats POSTed from agent
// 2) From workspace apps / rpty -> workspace app stats (from coderd / wsproxy)
// Ideally we would have a single code path for this.
uniqueIDs := slice.Unique(batch.WorkspaceID)
if err := tx.BatchUpdateWorkspaceLastUsedAt(ctx, database.BatchUpdateWorkspaceLastUsedAtParams{
IDs: uniqueIDs,
@@ -122,9 +140,11 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta
// nolint:revive // usage is a control flag while we have the experiment
func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspace database.WorkspaceIdentity, workspaceAgent database.WorkspaceAgent, stats *agentproto.Stats, usage bool) error {
// update agent stats
r.opts.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, stats, usage)
if !r.opts.DisableDatabaseInserts {
r.opts.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, stats, usage)
}
// update prometheus metrics
// update prometheus metrics (even if template insights are disabled)
if r.opts.UpdateAgentMetricsFn != nil {
r.opts.UpdateAgentMetricsFn(ctx, prometheusmetrics.AgentMetricLabels{
Username: workspace.OwnerUsername,
@@ -135,7 +155,10 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac
}
// workspace activity: if no sessions we do not bump activity
if usage && stats.SessionCountVscode == 0 && stats.SessionCountJetbrains == 0 && stats.SessionCountReconnectingPty == 0 && stats.SessionCountSsh == 0 {
if usage && stats.SessionCountVscode == 0 &&
stats.SessionCountJetbrains == 0 &&
stats.SessionCountReconnectingPty == 0 &&
stats.SessionCountSsh == 0 {
return nil
}