mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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] {
|
||||
|
||||
Generated
+11
@@ -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": {
|
||||
|
||||
Generated
+11
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-15T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [],
|
||||
"users": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user