Skip to content

Commit f2963cd

Browse files
committed
chore: add usage tracking package
1 parent 4bced62 commit f2963cd

23 files changed

+3708
-1815
lines changed

CODEOWNERS

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ tailnet/proto/ @spikecurtis @johnstcn
77
vpn/vpn.proto @spikecurtis @johnstcn
88
vpn/version.go @spikecurtis @johnstcn
99

10-
1110
# This caching code is particularly tricky, and one must be very careful when
1211
# altering it.
1312
coderd/files/ @aslilac
@@ -29,3 +28,8 @@ site/src/api/countriesGenerated.ts
2928
site/src/api/rbacresourcesGenerated.ts
3029
site/src/api/typesGenerated.ts
3130
site/CLAUDE.md
31+
32+
# Usage tracking code requires intimate knowledge of Tallyman and Metronome, as
33+
# well as guidance from revenue.
34+
coderd/usage/ @deansheather
35+
enterprise/coderd/usage/ @deansheather

coderd/database/dbauthz/dbauthz.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3913,6 +3913,13 @@ func (q *querier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg dat
39133913
return q.db.InsertTemplateVersionWorkspaceTag(ctx, arg)
39143914
}
39153915

3916+
func (q *querier) InsertUsageEvent(ctx context.Context, arg database.InsertUsageEventParams) error {
3917+
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
3918+
return err
3919+
}
3920+
return q.db.InsertUsageEvent(ctx, arg)
3921+
}
3922+
39163923
func (q *querier) InsertUser(ctx context.Context, arg database.InsertUserParams) (database.User, error) {
39173924
// Always check if the assigned roles can actually be assigned by this actor.
39183925
impliedRoles := append([]rbac.RoleIdentifier{rbac.RoleMember()}, q.convertToDeploymentRoles(arg.RBACRoles)...)
@@ -4260,6 +4267,14 @@ func (q *querier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string)
42604267
return q.db.RevokeDBCryptKey(ctx, activeKeyDigest)
42614268
}
42624269

4270+
func (q *querier) SelectUsageEventsForPublishing(ctx context.Context, arg time.Time) ([]database.UsageEvent, error) {
4271+
// ActionUpdate because we're updating the publish_started_at column.
4272+
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
4273+
return nil, err
4274+
}
4275+
return q.db.SelectUsageEventsForPublishing(ctx, arg)
4276+
}
4277+
42634278
func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) {
42644279
return q.db.TryAcquireLock(ctx, id)
42654280
}
@@ -4725,6 +4740,13 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da
47254740
return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg)
47264741
}
47274742

4743+
func (q *querier) UpdateUsageEventsPostPublish(ctx context.Context, arg database.UpdateUsageEventsPostPublishParams) error {
4744+
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
4745+
return err
4746+
}
4747+
return q.db.UpdateUsageEventsPostPublish(ctx, arg)
4748+
}
4749+
47284750
func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
47294751
return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id)
47304752
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5845,3 +5845,27 @@ func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() {
58455845
}).Asserts(w, policy.ActionUpdate, w.AsPrebuild(), policy.ActionUpdate)
58465846
}))
58475847
}
5848+
5849+
func (s *MethodTestSuite) TestUsageEvents() {
5850+
s.Run("InsertUsageEvent", s.Subtest(func(db database.Store, check *expects) {
5851+
check.Args(database.InsertUsageEventParams{
5852+
ID: "1",
5853+
EventType: database.UsageEventTypeDcManagedAgentsV1,
5854+
EventData: []byte("{}"),
5855+
CreatedAt: dbtime.Now(),
5856+
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
5857+
}))
5858+
5859+
s.Run("SelectUsageEventsForPublishing", s.Subtest(func(db database.Store, check *expects) {
5860+
check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
5861+
}))
5862+
5863+
s.Run("UpdateUsageEventsPostPublish", s.Subtest(func(db database.Store, check *expects) {
5864+
check.Args(database.UpdateUsageEventsPostPublishParams{
5865+
Now: dbtime.Now(),
5866+
IDs: []string{"1", "2"},
5867+
FailureMessages: []string{"error", "error"},
5868+
SetPublishedAts: []bool{false, false},
5869+
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
5870+
}))
5871+
}

coderd/database/dbmetrics/querymetrics.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dump.sql

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE usage_events;
2+
DROP TYPE usage_event_type;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
CREATE TYPE usage_event_type AS ENUM (
2+
'dc_managed_agents_v1'
3+
);
4+
5+
COMMENT ON TYPE usage_event_type IS 'The usage event type with version. "dc" means "discrete" (e.g. a single event, for counters), "hb" means "heartbeat" (e.g. a recurring event that contains a total count of usage generated from the database, for gauges).';
6+
7+
CREATE TABLE usage_events (
8+
id TEXT PRIMARY KEY,
9+
event_type usage_event_type NOT NULL,
10+
event_data JSONB NOT NULL,
11+
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
12+
publish_started_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
13+
published_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
14+
failure_message TEXT DEFAULT NULL
15+
);
16+
17+
COMMENT ON TABLE usage_events IS 'usage_events contains usage data that is collected from the product and potentially shipped to the usage collector service.';
18+
COMMENT ON COLUMN usage_events.id IS 'For "discrete" event types, this is a random UUID. For "heartbeat" event types, this is a combination of the event type and a truncated timestamp.';
19+
COMMENT ON COLUMN usage_events.event_data IS 'Event payload. Determined by the matching usage struct for this event type.';
20+
COMMENT ON COLUMN usage_events.publish_started_at IS 'Set to a timestamp while the event is being published by a Coder replica to the usage collector service. Used to avoid duplicate publishes by multiple replicas. Timestamps older than 1 hour are considered expired.';
21+
COMMENT ON COLUMN usage_events.published_at IS 'Set to a timestamp when the event is successfully (or permanently unsuccessfully) published to the usage collector service. If set, the event should never be attempted to be published again.';
22+
COMMENT ON COLUMN usage_events.failure_message IS 'Set to an error message when the event is temporarily or permanently unsuccessfully published to the usage collector service.';
23+
24+
CREATE INDEX idx_usage_events_created_at ON usage_events (created_at);
25+
CREATE INDEX idx_usage_events_publish_started_at ON usage_events (publish_started_at);
26+
CREATE INDEX idx_usage_events_published_at ON usage_events (published_at);

coderd/database/modelmethods.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/hex"
55
"sort"
66
"strconv"
7+
"strings"
78
"time"
89

910
"github.com/google/uuid"
@@ -628,3 +629,11 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce(
628629

629630
return m.DebouncedUntil, false
630631
}
632+
633+
func (e UsageEventType) IsDiscrete() bool {
634+
return e.Valid() && strings.HasPrefix(string(e), "dc_")
635+
}
636+
637+
func (e UsageEventType) IsHeartbeat() bool {
638+
return e.Valid() && strings.HasPrefix(string(e), "hb_")
639+
}

coderd/database/models.go

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy