mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
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:
@ -864,6 +864,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
BuiltinPostgres: builtinPostgres,
|
BuiltinPostgres: builtinPostgres,
|
||||||
DeploymentID: deploymentID,
|
DeploymentID: deploymentID,
|
||||||
Database: options.Database,
|
Database: options.Database,
|
||||||
|
Experiments: coderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value()),
|
||||||
Logger: logger.Named("telemetry"),
|
Logger: logger.Named("telemetry"),
|
||||||
URL: vals.Telemetry.URL.Value(),
|
URL: vals.Telemetry.URL.Value(),
|
||||||
Tunnel: tunnel != nil,
|
Tunnel: tunnel != nil,
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/buildinfo"
|
"github.com/coder/coder/v2/buildinfo"
|
||||||
clitelemetry "github.com/coder/coder/v2/cli/telemetry"
|
clitelemetry "github.com/coder/coder/v2/cli/telemetry"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
@ -48,6 +49,7 @@ type Options struct {
|
|||||||
Logger slog.Logger
|
Logger slog.Logger
|
||||||
// URL is an endpoint to direct telemetry towards!
|
// URL is an endpoint to direct telemetry towards!
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
|
Experiments codersdk.Experiments
|
||||||
|
|
||||||
DeploymentID string
|
DeploymentID string
|
||||||
DeploymentConfig *codersdk.DeploymentValues
|
DeploymentConfig *codersdk.DeploymentValues
|
||||||
@ -683,6 +685,52 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
|
|||||||
}
|
}
|
||||||
return nil
|
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()
|
err := eg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1152,6 +1200,7 @@ type Snapshot struct {
|
|||||||
Organizations []Organization `json:"organizations"`
|
Organizations []Organization `json:"organizations"`
|
||||||
TelemetryItems []TelemetryItem `json:"telemetry_items"`
|
TelemetryItems []TelemetryItem `json:"telemetry_items"`
|
||||||
UserTailnetConnections []UserTailnetConnection `json:"user_tailnet_connections"`
|
UserTailnetConnections []UserTailnetConnection `json:"user_tailnet_connections"`
|
||||||
|
PrebuiltWorkspaces []PrebuiltWorkspace `json:"prebuilt_workspaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deployment contains information about the host running Coder.
|
// Deployment contains information about the host running Coder.
|
||||||
@ -1724,6 +1773,21 @@ type UserTailnetConnection struct {
|
|||||||
CoderDesktopVersion *string `json:"coder_desktop_version"`
|
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{}
|
type noopReporter struct{}
|
||||||
|
|
||||||
func (*noopReporter) Report(_ *Snapshot) {}
|
func (*noopReporter) Report(_ *Snapshot) {}
|
||||||
|
@ -370,6 +370,113 @@ func TestTelemetryItem(t *testing.T) {
|
|||||||
require.Equal(t, item.Value, "new_value")
|
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) {
|
func TestShouldReportTelemetryDisabled(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled |
|
// Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled |
|
||||||
|
Reference in New Issue
Block a user