feat: add prebuilt workspaces telemetry (#18084)

Adds telemetry for a _global_ account of prebuilt workspaces created,
failed to build, and claimed.

Partitioning this data by template/preset tuple is not currently in
scope.

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
This commit is contained in:
Danny Kopping
2025-05-29 13:13:44 +02:00
committed by GitHub
parent 69c90064a8
commit bc83de2a72
3 changed files with 173 additions and 1 deletions

View File

@ -864,6 +864,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Experiments: coderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value()),
Logger: logger.Named("telemetry"),
URL: vals.Telemetry.URL.Value(),
Tunnel: tunnel != nil,

View File

@ -28,6 +28,7 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
"cdr.dev/slog"
"github.com/coder/coder/v2/buildinfo"
clitelemetry "github.com/coder/coder/v2/cli/telemetry"
"github.com/coder/coder/v2/coderd/database"
@ -47,7 +48,8 @@ type Options struct {
Database database.Store
Logger slog.Logger
// URL is an endpoint to direct telemetry towards!
URL *url.URL
URL *url.URL
Experiments codersdk.Experiments
DeploymentID string
DeploymentConfig *codersdk.DeploymentValues
@ -683,6 +685,52 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
}
return nil
})
eg.Go(func() error {
if !r.options.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds) {
return nil
}
metrics, err := r.options.Database.GetPrebuildMetrics(ctx)
if err != nil {
return xerrors.Errorf("get prebuild metrics: %w", err)
}
var totalCreated, totalFailed, totalClaimed int64
for _, metric := range metrics {
totalCreated += metric.CreatedCount
totalFailed += metric.FailedCount
totalClaimed += metric.ClaimedCount
}
snapshot.PrebuiltWorkspaces = make([]PrebuiltWorkspace, 0, 3)
now := dbtime.Now()
if totalCreated > 0 {
snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{
ID: uuid.New(),
CreatedAt: now,
EventType: PrebuiltWorkspaceEventTypeCreated,
Count: int(totalCreated),
})
}
if totalFailed > 0 {
snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{
ID: uuid.New(),
CreatedAt: now,
EventType: PrebuiltWorkspaceEventTypeFailed,
Count: int(totalFailed),
})
}
if totalClaimed > 0 {
snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{
ID: uuid.New(),
CreatedAt: now,
EventType: PrebuiltWorkspaceEventTypeClaimed,
Count: int(totalClaimed),
})
}
return nil
})
err := eg.Wait()
if err != nil {
@ -1152,6 +1200,7 @@ type Snapshot struct {
Organizations []Organization `json:"organizations"`
TelemetryItems []TelemetryItem `json:"telemetry_items"`
UserTailnetConnections []UserTailnetConnection `json:"user_tailnet_connections"`
PrebuiltWorkspaces []PrebuiltWorkspace `json:"prebuilt_workspaces"`
}
// Deployment contains information about the host running Coder.
@ -1724,6 +1773,21 @@ type UserTailnetConnection struct {
CoderDesktopVersion *string `json:"coder_desktop_version"`
}
type PrebuiltWorkspaceEventType string
const (
PrebuiltWorkspaceEventTypeCreated PrebuiltWorkspaceEventType = "created"
PrebuiltWorkspaceEventTypeFailed PrebuiltWorkspaceEventType = "failed"
PrebuiltWorkspaceEventTypeClaimed PrebuiltWorkspaceEventType = "claimed"
)
type PrebuiltWorkspace struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
EventType PrebuiltWorkspaceEventType `json:"event_type"`
Count int `json:"count"`
}
type noopReporter struct{}
func (*noopReporter) Report(_ *Snapshot) {}

View File

@ -370,6 +370,113 @@ func TestTelemetryItem(t *testing.T) {
require.Equal(t, item.Value, "new_value")
}
func TestPrebuiltWorkspacesTelemetry(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
db, _ := dbtestutil.NewDB(t)
cases := []struct {
name string
experimentEnabled bool
storeFn func(store database.Store) database.Store
expectedSnapshotEntries int
expectedCreated int
expectedFailed int
expectedClaimed int
}{
{
name: "experiment enabled",
experimentEnabled: true,
storeFn: func(store database.Store) database.Store {
return &mockDB{Store: store}
},
expectedSnapshotEntries: 3,
expectedCreated: 5,
expectedFailed: 2,
expectedClaimed: 3,
},
{
name: "experiment enabled, prebuilds not used",
experimentEnabled: true,
storeFn: func(store database.Store) database.Store {
return &emptyMockDB{Store: store}
},
},
{
name: "experiment disabled",
experimentEnabled: false,
storeFn: func(store database.Store) database.Store {
return &mockDB{Store: store}
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options {
opts.Database = tc.storeFn(db)
if tc.experimentEnabled {
opts.Experiments = codersdk.Experiments{
codersdk.ExperimentWorkspacePrebuilds,
}
}
return opts
})
require.NotNil(t, deployment)
require.NotNil(t, snapshot)
require.Len(t, snapshot.PrebuiltWorkspaces, tc.expectedSnapshotEntries)
eventCounts := make(map[telemetry.PrebuiltWorkspaceEventType]int)
for _, event := range snapshot.PrebuiltWorkspaces {
eventCounts[event.EventType] = event.Count
require.NotEqual(t, uuid.Nil, event.ID)
require.False(t, event.CreatedAt.IsZero())
}
require.Equal(t, tc.expectedCreated, eventCounts[telemetry.PrebuiltWorkspaceEventTypeCreated])
require.Equal(t, tc.expectedFailed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeFailed])
require.Equal(t, tc.expectedClaimed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeClaimed])
})
}
}
type mockDB struct {
database.Store
}
func (*mockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) {
return []database.GetPrebuildMetricsRow{
{
TemplateName: "template1",
PresetName: "preset1",
OrganizationName: "org1",
CreatedCount: 3,
FailedCount: 1,
ClaimedCount: 2,
},
{
TemplateName: "template2",
PresetName: "preset2",
OrganizationName: "org1",
CreatedCount: 2,
FailedCount: 1,
ClaimedCount: 1,
},
}, nil
}
type emptyMockDB struct {
database.Store
}
func (*emptyMockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) {
return []database.GetPrebuildMetricsRow{}, nil
}
func TestShouldReportTelemetryDisabled(t *testing.T) {
t.Parallel()
// Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled |