Repository: coder/coder
Pull request #16776: chore: track workspace resource monitors in telemetry
Author: login: hugodutka
Created: 2025-03-03T15:35:26Z | Merged: 2025-03-03T17:41:02Z

Addresses https://github.com/coder/nexus/issues/195. Specifically, just the "tracking templates" requirement:

> ## Tracking in templates
> To enable resource alerts, a user must add the resource_monitoring block to a template's coder_agent resource. We'd like to track if customers have any resource monitoring enabled on a per-deployment basis. Even better, we could identify which templates are using resource monitoring.


Diff:
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index a39ba8d4172f0..ba19184dc1a94 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -1438,6 +1438,17 @@ func (q *querier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agen
 	return q.db.FetchMemoryResourceMonitorsByAgentID(ctx, agentID)
 }
 
+func (q *querier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) {
+	// Ideally, we would return a list of monitors that the user has access to. However, that check would need to
+	// be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query
+	// was introduced for telemetry, we perform a simpler check.
+	if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil {
+		return nil, err
+	}
+
+	return q.db.FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt)
+}
+
 func (q *querier) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) {
 	if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationMessage); err != nil {
 		return database.FetchNewMessageMetadataRow{}, err
@@ -1459,6 +1470,17 @@ func (q *querier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, age
 	return q.db.FetchVolumesResourceMonitorsByAgentID(ctx, agentID)
 }
 
+func (q *querier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) {
+	// Ideally, we would return a list of monitors that the user has access to. However, that check would need to
+	// be implemented similarly to GetWorkspaces, which is more complex than what we're doing here. Since this query
+	// was introduced for telemetry, we perform a simpler check.
+	if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil {
+		return nil, err
+	}
+
+	return q.db.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt)
+}
+
 func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
 	return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
 }
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index 12d6d8804e3e4..a2ac739042366 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -4919,6 +4919,14 @@ func (s *MethodTestSuite) TestResourcesMonitor() {
 		}).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionUpdate)
 	}))
 
+	s.Run("FetchMemoryResourceMonitorsUpdatedAfter", s.Subtest(func(db database.Store, check *expects) {
+		check.Args(dbtime.Now()).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionRead)
+	}))
+
+	s.Run("FetchVolumesResourceMonitorsUpdatedAfter", s.Subtest(func(db database.Store, check *expects) {
+		check.Args(dbtime.Now()).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionRead)
+	}))
+
 	s.Run("FetchMemoryResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) {
 		agt, w := createAgent(s.T(), db)
 
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index cc559a7e77f16..b750b6a22a572 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -2391,6 +2391,19 @@ func (q *FakeQuerier) FetchMemoryResourceMonitorsByAgentID(_ context.Context, ag
 	return database.WorkspaceAgentMemoryResourceMonitor{}, sql.ErrNoRows
 }
 
+func (q *FakeQuerier) FetchMemoryResourceMonitorsUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) {
+	q.mutex.RLock()
+	defer q.mutex.RUnlock()
+
+	monitors := []database.WorkspaceAgentMemoryResourceMonitor{}
+	for _, monitor := range q.workspaceAgentMemoryResourceMonitors {
+		if monitor.UpdatedAt.After(updatedAt) {
+			monitors = append(monitors, monitor)
+		}
+	}
+	return monitors, nil
+}
+
 func (q *FakeQuerier) FetchNewMessageMetadata(_ context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) {
 	err := validateDatabaseType(arg)
 	if err != nil {
@@ -2435,6 +2448,19 @@ func (q *FakeQuerier) FetchVolumesResourceMonitorsByAgentID(_ context.Context, a
 	return monitors, nil
 }
 
+func (q *FakeQuerier) FetchVolumesResourceMonitorsUpdatedAfter(_ context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) {
+	q.mutex.RLock()
+	defer q.mutex.RUnlock()
+
+	monitors := []database.WorkspaceAgentVolumeResourceMonitor{}
+	for _, monitor := range q.workspaceAgentVolumeResourceMonitors {
+		if monitor.UpdatedAt.After(updatedAt) {
+			monitors = append(monitors, monitor)
+		}
+	}
+	return monitors, nil
+}
+
 func (q *FakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) {
 	q.mutex.RLock()
 	defer q.mutex.RUnlock()
diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go
index d05ec5f5acdf9..ab6fbe5174e7f 100644
--- a/coderd/database/dbmetrics/querymetrics.go
+++ b/coderd/database/dbmetrics/querymetrics.go
@@ -451,6 +451,13 @@ func (m queryMetricsStore) FetchMemoryResourceMonitorsByAgentID(ctx context.Cont
 	return r0, r1
 }
 
+func (m queryMetricsStore) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) {
+	start := time.Now()
+	r0, r1 := m.s.FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt)
+	m.queryLatencies.WithLabelValues("FetchMemoryResourceMonitorsUpdatedAfter").Observe(time.Since(start).Seconds())
+	return r0, r1
+}
+
 func (m queryMetricsStore) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) {
 	start := time.Now()
 	r0, r1 := m.s.FetchNewMessageMetadata(ctx, arg)
@@ -465,6 +472,13 @@ func (m queryMetricsStore) FetchVolumesResourceMonitorsByAgentID(ctx context.Con
 	return r0, r1
 }
 
+func (m queryMetricsStore) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) {
+	start := time.Now()
+	r0, r1 := m.s.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt)
+	m.queryLatencies.WithLabelValues("FetchVolumesResourceMonitorsUpdatedAfter").Observe(time.Since(start).Seconds())
+	return r0, r1
+}
+
 func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
 	start := time.Now()
 	apiKey, err := m.s.GetAPIKeyByID(ctx, id)
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index 39f148d90e20e..46e4dbbf4ea2a 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -787,6 +787,21 @@ func (mr *MockStoreMockRecorder) FetchMemoryResourceMonitorsByAgentID(ctx, agent
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchMemoryResourceMonitorsByAgentID", reflect.TypeOf((*MockStore)(nil).FetchMemoryResourceMonitorsByAgentID), ctx, agentID)
 }
 
+// FetchMemoryResourceMonitorsUpdatedAfter mocks base method.
+func (m *MockStore) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentMemoryResourceMonitor, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "FetchMemoryResourceMonitorsUpdatedAfter", ctx, updatedAt)
+	ret0, _ := ret[0].([]database.WorkspaceAgentMemoryResourceMonitor)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// FetchMemoryResourceMonitorsUpdatedAfter indicates an expected call of FetchMemoryResourceMonitorsUpdatedAfter.
+func (mr *MockStoreMockRecorder) FetchMemoryResourceMonitorsUpdatedAfter(ctx, updatedAt any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchMemoryResourceMonitorsUpdatedAfter", reflect.TypeOf((*MockStore)(nil).FetchMemoryResourceMonitorsUpdatedAfter), ctx, updatedAt)
+}
+
 // FetchNewMessageMetadata mocks base method.
 func (m *MockStore) FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error) {
 	m.ctrl.T.Helper()
@@ -817,6 +832,21 @@ func (mr *MockStoreMockRecorder) FetchVolumesResourceMonitorsByAgentID(ctx, agen
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVolumesResourceMonitorsByAgentID", reflect.TypeOf((*MockStore)(nil).FetchVolumesResourceMonitorsByAgentID), ctx, agentID)
 }
 
+// FetchVolumesResourceMonitorsUpdatedAfter mocks base method.
+func (m *MockStore) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]database.WorkspaceAgentVolumeResourceMonitor, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "FetchVolumesResourceMonitorsUpdatedAfter", ctx, updatedAt)
+	ret0, _ := ret[0].([]database.WorkspaceAgentVolumeResourceMonitor)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// FetchVolumesResourceMonitorsUpdatedAfter indicates an expected call of FetchVolumesResourceMonitorsUpdatedAfter.
+func (mr *MockStoreMockRecorder) FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVolumesResourceMonitorsUpdatedAfter", reflect.TypeOf((*MockStore)(nil).FetchVolumesResourceMonitorsUpdatedAfter), ctx, updatedAt)
+}
+
 // GetAPIKeyByID mocks base method.
 func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
 	m.ctrl.T.Helper()
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 6bae27ec1f3d4..4fe20f3fcd806 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -113,9 +113,11 @@ type sqlcQuerier interface {
 	EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error
 	FavoriteWorkspace(ctx context.Context, id uuid.UUID) error
 	FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentMemoryResourceMonitor, error)
+	FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error)
 	// This is used to build up the notification_message's JSON payload.
 	FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error)
 	FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error)
+	FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error)
 	GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
 	// there is no unique constraint on empty token names
 	GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 0891bc8c9fcc6..1bc0261c1755a 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -12372,6 +12372,46 @@ func (q *sqlQuerier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, a
 	return i, err
 }
 
+const fetchMemoryResourceMonitorsUpdatedAfter = `-- name: FetchMemoryResourceMonitorsUpdatedAfter :many
+SELECT
+	agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
+FROM
+	workspace_agent_memory_resource_monitors
+WHERE
+	updated_at > $1
+`
+
+func (q *sqlQuerier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error) {
+	rows, err := q.db.QueryContext(ctx, fetchMemoryResourceMonitorsUpdatedAfter, updatedAt)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []WorkspaceAgentMemoryResourceMonitor
+	for rows.Next() {
+		var i WorkspaceAgentMemoryResourceMonitor
+		if err := rows.Scan(
+			&i.AgentID,
+			&i.Enabled,
+			&i.Threshold,
+			&i.CreatedAt,
+			&i.UpdatedAt,
+			&i.State,
+			&i.DebouncedUntil,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
 const fetchVolumesResourceMonitorsByAgentID = `-- name: FetchVolumesResourceMonitorsByAgentID :many
 SELECT
 	agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
@@ -12413,6 +12453,47 @@ func (q *sqlQuerier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context,
 	return items, nil
 }
 
+const fetchVolumesResourceMonitorsUpdatedAfter = `-- name: FetchVolumesResourceMonitorsUpdatedAfter :many
+SELECT
+	agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
+FROM
+	workspace_agent_volume_resource_monitors
+WHERE
+	updated_at > $1
+`
+
+func (q *sqlQuerier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error) {
+	rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsUpdatedAfter, updatedAt)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []WorkspaceAgentVolumeResourceMonitor
+	for rows.Next() {
+		var i WorkspaceAgentVolumeResourceMonitor
+		if err := rows.Scan(
+			&i.AgentID,
+			&i.Enabled,
+			&i.Threshold,
+			&i.Path,
+			&i.CreatedAt,
+			&i.UpdatedAt,
+			&i.State,
+			&i.DebouncedUntil,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
 const insertMemoryResourceMonitor = `-- name: InsertMemoryResourceMonitor :one
 INSERT INTO
 	workspace_agent_memory_resource_monitors (
diff --git a/coderd/database/queries/workspaceagentresourcemonitors.sql b/coderd/database/queries/workspaceagentresourcemonitors.sql
index 84ee5c67b37ef..50e7e818f7c67 100644
--- a/coderd/database/queries/workspaceagentresourcemonitors.sql
+++ b/coderd/database/queries/workspaceagentresourcemonitors.sql
@@ -1,3 +1,19 @@
+-- name: FetchVolumesResourceMonitorsUpdatedAfter :many
+SELECT
+	*
+FROM
+	workspace_agent_volume_resource_monitors
+WHERE
+	updated_at > $1;
+
+-- name: FetchMemoryResourceMonitorsUpdatedAfter :many
+SELECT
+	*
+FROM
+	workspace_agent_memory_resource_monitors
+WHERE
+	updated_at > $1;
+
 -- name: FetchMemoryResourceMonitorsByAgentID :one
 SELECT
 	*
diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go
index e3d50da29e5cb..8956fed23990e 100644
--- a/coderd/telemetry/telemetry.go
+++ b/coderd/telemetry/telemetry.go
@@ -624,6 +624,28 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
 		}
 		return nil
 	})
+	eg.Go(func() error {
+		memoryMonitors, err := r.options.Database.FetchMemoryResourceMonitorsUpdatedAfter(ctx, createdAfter)
+		if err != nil {
+			return xerrors.Errorf("get memory resource monitors: %w", err)
+		}
+		snapshot.WorkspaceAgentMemoryResourceMonitors = make([]WorkspaceAgentMemoryResourceMonitor, 0, len(memoryMonitors))
+		for _, monitor := range memoryMonitors {
+			snapshot.WorkspaceAgentMemoryResourceMonitors = append(snapshot.WorkspaceAgentMemoryResourceMonitors, ConvertWorkspaceAgentMemoryResourceMonitor(monitor))
+		}
+		return nil
+	})
+	eg.Go(func() error {
+		volumeMonitors, err := r.options.Database.FetchVolumesResourceMonitorsUpdatedAfter(ctx, createdAfter)
+		if err != nil {
+			return xerrors.Errorf("get volume resource monitors: %w", err)
+		}
+		snapshot.WorkspaceAgentVolumeResourceMonitors = make([]WorkspaceAgentVolumeResourceMonitor, 0, len(volumeMonitors))
+		for _, monitor := range volumeMonitors {
+			snapshot.WorkspaceAgentVolumeResourceMonitors = append(snapshot.WorkspaceAgentVolumeResourceMonitors, ConvertWorkspaceAgentVolumeResourceMonitor(monitor))
+		}
+		return nil
+	})
 	eg.Go(func() error {
 		proxies, err := r.options.Database.GetWorkspaceProxies(ctx)
 		if err != nil {
@@ -765,6 +787,26 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
 	return snapAgent
 }
 
+func ConvertWorkspaceAgentMemoryResourceMonitor(monitor database.WorkspaceAgentMemoryResourceMonitor) WorkspaceAgentMemoryResourceMonitor {
+	return WorkspaceAgentMemoryResourceMonitor{
+		AgentID:   monitor.AgentID,
+		Enabled:   monitor.Enabled,
+		Threshold: monitor.Threshold,
+		CreatedAt: monitor.CreatedAt,
+		UpdatedAt: monitor.UpdatedAt,
+	}
+}
+
+func ConvertWorkspaceAgentVolumeResourceMonitor(monitor database.WorkspaceAgentVolumeResourceMonitor) WorkspaceAgentVolumeResourceMonitor {
+	return WorkspaceAgentVolumeResourceMonitor{
+		AgentID:   monitor.AgentID,
+		Enabled:   monitor.Enabled,
+		Threshold: monitor.Threshold,
+		CreatedAt: monitor.CreatedAt,
+		UpdatedAt: monitor.UpdatedAt,
+	}
+}
+
 // ConvertWorkspaceAgentStat anonymizes a workspace agent stat.
 func ConvertWorkspaceAgentStat(stat database.GetWorkspaceAgentStatsRow) WorkspaceAgentStat {
 	return WorkspaceAgentStat{
@@ -1083,28 +1125,30 @@ func ConvertTelemetryItem(item database.TelemetryItem) TelemetryItem {
 type Snapshot struct {
 	DeploymentID string `json:"deployment_id"`
 
-	APIKeys                   []APIKey                    `json:"api_keys"`
-	CLIInvocations            []clitelemetry.Invocation   `json:"cli_invocations"`
-	ExternalProvisioners      []ExternalProvisioner       `json:"external_provisioners"`
-	Licenses                  []License                   `json:"licenses"`
-	ProvisionerJobs           []ProvisionerJob            `json:"provisioner_jobs"`
-	TemplateVersions          []TemplateVersion           `json:"template_versions"`
-	Templates                 []Template                  `json:"templates"`
-	Users                     []User                      `json:"users"`
-	Groups                    []Group                     `json:"groups"`
-	GroupMembers              []GroupMember               `json:"group_members"`
-	WorkspaceAgentStats       []WorkspaceAgentStat        `json:"workspace_agent_stats"`
-	WorkspaceAgents           []WorkspaceAgent            `json:"workspace_agents"`
-	WorkspaceApps             []WorkspaceApp              `json:"workspace_apps"`
-	WorkspaceBuilds           []WorkspaceBuild            `json:"workspace_build"`
-	WorkspaceProxies          []WorkspaceProxy            `json:"workspace_proxies"`
-	WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"`
-	WorkspaceResources        []WorkspaceResource         `json:"workspace_resources"`
-	WorkspaceModules          []WorkspaceModule           `json:"workspace_modules"`
-	Workspaces                []Workspace                 `json:"workspaces"`
-	NetworkEvents             []NetworkEvent              `json:"network_events"`
-	Organizations             []Organization              `json:"organizations"`
-	TelemetryItems            []TelemetryItem             `json:"telemetry_items"`
+	APIKeys                              []APIKey                              `json:"api_keys"`
+	CLIInvocations                       []clitelemetry.Invocation             `json:"cli_invocations"`
+	ExternalProvisioners                 []ExternalProvisioner                 `json:"external_provisioners"`
+	Licenses                             []License                             `json:"licenses"`
+	ProvisionerJobs                      []ProvisionerJob                      `json:"provisioner_jobs"`
+	TemplateVersions                     []TemplateVersion                     `json:"template_versions"`
+	Templates                            []Template                            `json:"templates"`
+	Users                                []User                                `json:"users"`
+	Groups                               []Group                               `json:"groups"`
+	GroupMembers                         []GroupMember                         `json:"group_members"`
+	WorkspaceAgentStats                  []WorkspaceAgentStat                  `json:"workspace_agent_stats"`
+	WorkspaceAgents                      []WorkspaceAgent                      `json:"workspace_agents"`
+	WorkspaceApps                        []WorkspaceApp                        `json:"workspace_apps"`
+	WorkspaceBuilds                      []WorkspaceBuild                      `json:"workspace_build"`
+	WorkspaceProxies                     []WorkspaceProxy                      `json:"workspace_proxies"`
+	WorkspaceResourceMetadata            []WorkspaceResourceMetadata           `json:"workspace_resource_metadata"`
+	WorkspaceResources                   []WorkspaceResource                   `json:"workspace_resources"`
+	WorkspaceAgentMemoryResourceMonitors []WorkspaceAgentMemoryResourceMonitor `json:"workspace_agent_memory_resource_monitors"`
+	WorkspaceAgentVolumeResourceMonitors []WorkspaceAgentVolumeResourceMonitor `json:"workspace_agent_volume_resource_monitors"`
+	WorkspaceModules                     []WorkspaceModule                     `json:"workspace_modules"`
+	Workspaces                           []Workspace                           `json:"workspaces"`
+	NetworkEvents                        []NetworkEvent                        `json:"network_events"`
+	Organizations                        []Organization                        `json:"organizations"`
+	TelemetryItems                       []TelemetryItem                       `json:"telemetry_items"`
 }
 
 // Deployment contains information about the host running Coder.
@@ -1232,6 +1276,22 @@ type WorkspaceAgentStat struct {
 	SessionCountSSH             int64     `json:"session_count_ssh"`
 }
 
+type WorkspaceAgentMemoryResourceMonitor struct {
+	AgentID   uuid.UUID `json:"agent_id"`
+	Enabled   bool      `json:"enabled"`
+	Threshold int32     `json:"threshold"`
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+}
+
+type WorkspaceAgentVolumeResourceMonitor struct {
+	AgentID   uuid.UUID `json:"agent_id"`
+	Enabled   bool      `json:"enabled"`
+	Threshold int32     `json:"threshold"`
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
+}
+
 type WorkspaceApp struct {
 	ID        uuid.UUID `json:"id"`
 	CreatedAt time.Time `json:"created_at"`
diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go
index 29fcb644fc88f..6f97ce8a1270b 100644
--- a/coderd/telemetry/telemetry_test.go
+++ b/coderd/telemetry/telemetry_test.go
@@ -112,6 +112,8 @@ func TestTelemetry(t *testing.T) {
 		_, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
 
 		_ = dbgen.WorkspaceModule(t, db, database.WorkspaceModule{})
+		_ = dbgen.WorkspaceAgentMemoryResourceMonitor(t, db, database.WorkspaceAgentMemoryResourceMonitor{})
+		_ = dbgen.WorkspaceAgentVolumeResourceMonitor(t, db, database.WorkspaceAgentVolumeResourceMonitor{})
 
 		_, snapshot := collectSnapshot(t, db, nil)
 		require.Len(t, snapshot.ProvisionerJobs, 1)
@@ -133,6 +135,8 @@ func TestTelemetry(t *testing.T) {
 		require.Len(t, snapshot.Organizations, 1)
 		// We create one item manually above. The other is TelemetryEnabled, created by the snapshotter.
 		require.Len(t, snapshot.TelemetryItems, 2)
+		require.Len(t, snapshot.WorkspaceAgentMemoryResourceMonitors, 1)
+		require.Len(t, snapshot.WorkspaceAgentVolumeResourceMonitors, 1)
 		wsa := snapshot.WorkspaceAgents[0]
 		require.Len(t, wsa.Subsystems, 2)
 		require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0])


Reviews:
user: login: defelmnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:58:54Z
user: login: defelmnq
body: Just a comment which is more a question than a blocker - but all good ! 

Thanks a lot @hugodutka 🔥 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:59:21Z
user: login: hugodutka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T17:01:37Z
user: login: defelmnq
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T17:08:43Z

Review comments:
diff_hunk: @@ -765,6 +787,26 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
 	return snapAgent
 }
 
+func ConvertWorkspaceAgentMemoryResourceMonitor(monitor database.WorkspaceAgentMemoryResourceMonitor) WorkspaceAgentMemoryResourceMonitor {
+	return WorkspaceAgentMemoryResourceMonitor{
+		AgentID:   monitor.AgentID,
+		Enabled:   monitor.Enabled,
+		Threshold: monitor.Threshold,
+		CreatedAt: monitor.CreatedAt,
+		UpdatedAt: monitor.UpdatedAt,
+	}
+}
+
+func ConvertWorkspaceAgentVolumeResourceMonitor(monitor database.WorkspaceAgentVolumeResourceMonitor) WorkspaceAgentVolumeResourceMonitor {
+	return WorkspaceAgentVolumeResourceMonitor{
path: coderd/telemetry/telemetry.go
user: login: defelmnq
body: Would we have any interest of inserting the `path` here too ? 
created_at: 2025-03-03T16:58:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -765,6 +787,26 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
 	return snapAgent
 }
 
+func ConvertWorkspaceAgentMemoryResourceMonitor(monitor database.WorkspaceAgentMemoryResourceMonitor) WorkspaceAgentMemoryResourceMonitor {
+	return WorkspaceAgentMemoryResourceMonitor{
+		AgentID:   monitor.AgentID,
+		Enabled:   monitor.Enabled,
+		Threshold: monitor.Threshold,
+		CreatedAt: monitor.CreatedAt,
+		UpdatedAt: monitor.UpdatedAt,
+	}
+}
+
+func ConvertWorkspaceAgentVolumeResourceMonitor(monitor database.WorkspaceAgentVolumeResourceMonitor) WorkspaceAgentVolumeResourceMonitor {
+	return WorkspaceAgentVolumeResourceMonitor{
path: coderd/telemetry/telemetry.go
user: login: hugodutka
body: I feel like paths may contain sensitive info, so I decided to exclude it. I don't think we would have much use for it in the telemetry either.
created_at: 2025-03-03T17:01:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -765,6 +787,26 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
 	return snapAgent
 }
 
+func ConvertWorkspaceAgentMemoryResourceMonitor(monitor database.WorkspaceAgentMemoryResourceMonitor) WorkspaceAgentMemoryResourceMonitor {
+	return WorkspaceAgentMemoryResourceMonitor{
+		AgentID:   monitor.AgentID,
+		Enabled:   monitor.Enabled,
+		Threshold: monitor.Threshold,
+		CreatedAt: monitor.CreatedAt,
+		UpdatedAt: monitor.UpdatedAt,
+	}
+}
+
+func ConvertWorkspaceAgentVolumeResourceMonitor(monitor database.WorkspaceAgentVolumeResourceMonitor) WorkspaceAgentVolumeResourceMonitor {
+	return WorkspaceAgentVolumeResourceMonitor{
path: coderd/telemetry/telemetry.go
user: login: defelmnq
body: make sense indeed - all good to skip it as soon as we keep it in mind ✅ 
created_at: 2025-03-03T17:08:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: hugodutka
created_at: 2025-03-03T16:48:24Z
body: One idea after thinking about the queries here again: maybe it'd be better to use the updated at value rather than created at for fetching the monitors? We'd then also get telemetry updates whenever the monitors are updated.
author_association: CONTRIBUTOR
user: login: stirby
created_at: 2025-03-03T17:18:03Z
body: /cherry-pick release/2.20

author_association: COLLABORATOR
user: login: hugodutka
created_at: 2025-03-03T17:40:44Z
body: Updated the queries to use `updated_at` instead of `created_at` as explained in https://github.com/coder/coder/pull/16776#issuecomment-2694988511.
author_association: CONTRIBUTOR