From 89b1757e0864a68190497af8e3059b6ac9a7916c Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 30 Jul 2025 06:56:32 +0000 Subject: [PATCH] chore: wire up usage tracking for managed agents --- coderd/coderd.go | 14 +++++ .../provisionerdserver/provisionerdserver.go | 19 ++++++ .../provisionerdserver_test.go | 62 ++++++++++++++++++- enterprise/cli/server.go | 45 +++++++++++++- enterprise/coderd/coderd.go | 10 +++ enterprise/coderd/provisionerdaemons.go | 1 + enterprise/coderd/usage/collector.go | 18 +++--- enterprise/coderd/usage/collector_test.go | 4 +- enterprise/coderd/usage/publisher.go | 48 +++++++------- enterprise/coderd/usage/publisher_test.go | 22 +++---- 10 files changed, 190 insertions(+), 53 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 9115888fc566b..3096b07c72f50 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -21,6 +21,7 @@ import ( "github.com/coder/coder/v2/coderd/oauth2provider" "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/usage" "github.com/coder/coder/v2/coderd/wsbuilder" "github.com/andybalholm/brotli" @@ -198,6 +199,7 @@ type Options struct { TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] + UsageCollector *atomic.Pointer[usage.Collector] // CoordinatorResumeTokenProvider is used to provide and validate resume // tokens issued by and passed to the coordinator DRPC API. CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider @@ -426,6 +428,13 @@ func New(options *Options) *API { v := schedule.NewAGPLUserQuietHoursScheduleStore() options.UserQuietHoursScheduleStore.Store(&v) } + if options.UsageCollector == nil { + options.UsageCollector = &atomic.Pointer[usage.Collector]{} + } + if options.UsageCollector.Load() == nil { + collector := usage.NewAGPLCollector() + options.UsageCollector.Store(&collector) + } if options.OneTimePasscodeValidityPeriod == 0 { options.OneTimePasscodeValidityPeriod = 20 * time.Minute } @@ -588,6 +597,7 @@ func New(options *Options) *API { UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore, AccessControlStore: options.AccessControlStore, BuildUsageChecker: &buildUsageChecker, + UsageCollector: options.UsageCollector, FileCache: files.New(options.PrometheusRegistry, options.Authorizer), Experiments: experiments, WebpushDispatcher: options.WebPushDispatcher, @@ -1662,6 +1672,9 @@ type API struct { // BuildUsageChecker is a pointer as it's passed around to multiple // components. BuildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker] + // UsageCollector is a pointer to an atomic pointer because it is passed to + // multiple components. + UsageCollector *atomic.Pointer[usage.Collector] UpdatesProvider tailnet.WorkspaceUpdatesProvider @@ -1877,6 +1890,7 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n &api.Auditor, api.TemplateScheduleStore, api.UserQuietHoursScheduleStore, + api.UsageCollector, api.DeploymentValues, provisionerdserver.Options{ OIDCConfig: api.OIDCConfig, diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 518b48d2fe04b..ef34dd1719be7 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -29,6 +29,7 @@ import ( "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/usage" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk/drpcsdk" @@ -121,6 +122,7 @@ type server struct { DeploymentValues *codersdk.DeploymentValues NotificationsEnqueuer notifications.Enqueuer PrebuildsOrchestrator *atomic.Pointer[prebuilds.ReconciliationOrchestrator] + UsageCollector *atomic.Pointer[usage.Collector] OIDCConfig promoauth.OAuth2Config @@ -174,6 +176,7 @@ func NewServer( auditor *atomic.Pointer[audit.Auditor], templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore], userQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore], + usageCollector *atomic.Pointer[usage.Collector], deploymentValues *codersdk.DeploymentValues, options Options, enqueuer notifications.Enqueuer, @@ -195,6 +198,9 @@ func NewServer( if userQuietHoursScheduleStore == nil { return nil, xerrors.New("userQuietHoursScheduleStore is nil") } + if usageCollector == nil { + return nil, xerrors.New("usageCollector is nil") + } if deploymentValues == nil { return nil, xerrors.New("deploymentValues is nil") } @@ -244,6 +250,7 @@ func NewServer( heartbeatInterval: options.HeartbeatInterval, heartbeatFn: options.HeartbeatFn, PrebuildsOrchestrator: prebuildsOrchestrator, + UsageCollector: usageCollector, } if s.heartbeatFn == nil { @@ -1892,6 +1899,18 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro } sidebarAppID = uuid.NullUUID{UUID: id, Valid: true} + + // Collect usage event for managed agents. + usageCollector := s.UsageCollector.Load() + if usageCollector != nil { + event := usage.DCManagedAgentsV1{ + Count: 1, + } + err = (*usageCollector).CollectDiscreteUsageEvent(ctx, db, event) + if err != nil { + return xerrors.Errorf("collect %q event: %w", event.EventType(), err) + } + } } // Regardless of whether there is an AI task or not, update the field to indicate one way or the other since it diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 66684835650a8..6ed6072518923 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -44,6 +44,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/coderd/usage" "github.com/coder/coder/v2/coderd/wspubsub" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -67,6 +68,13 @@ func testUserQuietHoursScheduleStore() *atomic.Pointer[schedule.UserQuietHoursSc return ptr } +func testUsageCollector() *atomic.Pointer[usage.Collector] { + ptr := &atomic.Pointer[usage.Collector]{} + collector := usage.NewAGPLCollector() + ptr.Store(&collector) + return ptr +} + func TestAcquireJob_LongPoll(t *testing.T) { t.Parallel() //nolint:dogsled @@ -2469,7 +2477,10 @@ func TestCompleteJob(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - srv, db, _, pd := setup(t, false, &overrides{}) + fakeUsageCollector, usageCollectorPtr := newFakeUsageCollector() + srv, db, _, pd := setup(t, false, &overrides{ + usageCollector: usageCollectorPtr, + }) importJobID := uuid.New() tvID := uuid.New() @@ -2535,6 +2546,10 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) require.True(t, version.HasAITask.Valid) // We ALWAYS expect a value to be set, therefore not nil, i.e. valid = true. require.Equal(t, tc.expected, version.HasAITask.Bool) + + // We never expect a usage event to be collected for + // template imports. + require.Empty(t, fakeUsageCollector.collectedEvents) }) } }) @@ -2576,7 +2591,10 @@ func TestCompleteJob(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - srv, db, _, pd := setup(t, false, &overrides{}) + fakeUsageCollector, usageCollectorPtr := newFakeUsageCollector() + srv, db, _, pd := setup(t, false, &overrides{ + usageCollector: usageCollectorPtr, + }) importJobID := uuid.New() tvID := uuid.New() @@ -2657,6 +2675,15 @@ func TestCompleteJob(t *testing.T) { if tc.expected { require.Equal(t, sidebarAppID, build.AITaskSidebarAppID.UUID.String()) + + // Check that a usage event was collected. + require.Len(t, fakeUsageCollector.collectedEvents, 1) + require.Equal(t, usage.DCManagedAgentsV1{ + Count: 1, + }, fakeUsageCollector.collectedEvents[0]) + } else { + // Check that no usage event was collected. + require.Empty(t, fakeUsageCollector.collectedEvents) } }) } @@ -3582,6 +3609,7 @@ type overrides struct { externalAuthConfigs []*externalauth.Config templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore] userQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] + usageCollector *atomic.Pointer[usage.Collector] clock *quartz.Mock acquireJobLongPollDuration time.Duration heartbeatFn func(ctx context.Context) error @@ -3603,6 +3631,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi var externalAuthConfigs []*externalauth.Config tss := testTemplateScheduleStore() uqhss := testUserQuietHoursScheduleStore() + usageCollector := testUsageCollector() clock := quartz.NewReal() pollDur := time.Duration(0) if ov == nil { @@ -3640,6 +3669,15 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi require.True(t, swapped) } } + if ov.usageCollector != nil { + tusageCollector := usageCollector.Load() + // keep the initial test value if the override hasn't set the atomic pointer. + usageCollector = ov.usageCollector + if usageCollector.Load() == nil { + swapped := usageCollector.CompareAndSwap(nil, tusageCollector) + require.True(t, swapped) + } + } if ov.clock != nil { clock = ov.clock } @@ -3695,6 +3733,7 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi auditPtr, tss, uqhss, + usageCollector, deploymentValues, provisionerdserver.Options{ ExternalAuthConfigs: externalAuthConfigs, @@ -3809,3 +3848,22 @@ func (s *fakeStream) cancel() { s.canceled = true s.c.Broadcast() } + +type fakeUsageCollector struct { + collectedEvents []usage.Event +} + +var _ usage.Collector = &fakeUsageCollector{} + +func newFakeUsageCollector() (*fakeUsageCollector, *atomic.Pointer[usage.Collector]) { + ptr := &atomic.Pointer[usage.Collector]{} + fake := &fakeUsageCollector{} + var collector usage.Collector = fake + ptr.Store(&collector) + return fake, ptr +} + +func (f *fakeUsageCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, event usage.DiscreteEvent) error { + f.collectedEvents = append(f.collectedEvents, event) + return nil +} diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 3b1fd63ab1c4c..23047ae622e6f 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/v2/enterprise/audit/backends" "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/dormancy" + "github.com/coder/coder/v2/enterprise/coderd/usage" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/enterprise/trialer" "github.com/coder/coder/v2/tailnet" @@ -116,11 +117,33 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { o.ExternalTokenEncryption = cs } + if o.LicenseKeys == nil { + o.LicenseKeys = coderd.Keys + } + + multiCloser := &multiCloser{} + + // Create the enterprise API. api, err := coderd.New(ctx, o) if err != nil { return nil, nil, err } - return api.AGPL, api, nil + multiCloser.Add(api) + + // Start the enterprise usage publisher routine. This won't do anything + // unless the deployment is licensed and one of the licenses has usage + // publishing enabled. + publisher := usage.NewTallymanPublisher(ctx, options.Logger, options.Database, o.LicenseKeys, + usage.PublisherWithHTTPClient(api.HTTPClient), + ) + err = publisher.Start() + if err != nil { + _ = multiCloser.Close() + return nil, nil, xerrors.Errorf("start usage publisher: %w", err) + } + multiCloser.Add(publisher) + + return api.AGPL, multiCloser, nil }) cmd.AddSubcommands( @@ -128,3 +151,23 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { ) return cmd } + +type multiCloser struct { + closers []io.Closer +} + +var _ io.Closer = &multiCloser{} + +func (m *multiCloser) Add(closer io.Closer) { + m.closers = append(m.closers, closer) +} + +func (m *multiCloser) Close() error { + var mErr error + for _, closer := range m.closers { + if err := closer.Close(); err != nil { + mErr = xerrors.Errorf("close %T: %w", closer, err) + } + } + return mErr +} diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 9583e14cd7fd3..f4fd78ec006ac 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/coder/quartz" @@ -22,10 +23,12 @@ import ( agplportsharing "github.com/coder/coder/v2/coderd/portsharing" agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac/policy" + agplusage "github.com/coder/coder/v2/coderd/usage" "github.com/coder/coder/v2/coderd/wsbuilder" "github.com/coder/coder/v2/enterprise/coderd/connectionlog" "github.com/coder/coder/v2/enterprise/coderd/enidpsync" "github.com/coder/coder/v2/enterprise/coderd/portsharing" + "github.com/coder/coder/v2/enterprise/coderd/usage" "golang.org/x/xerrors" "tailscale.com/tailcfg" @@ -90,6 +93,13 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { if options.Entitlements == nil { options.Entitlements = entitlements.New() } + if options.Options.UsageCollector == nil { + options.Options.UsageCollector = &atomic.Pointer[agplusage.Collector]{} + } + if options.Options.UsageCollector.Load() == nil { + collector := usage.NewDBCollector() + options.Options.UsageCollector.Store(&collector) + } ctx, cancelFunc := context.WithCancel(ctx) diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index c8304952781d1..1d0f70dc92c9d 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -352,6 +352,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) &api.AGPL.Auditor, api.AGPL.TemplateScheduleStore, api.AGPL.UserQuietHoursScheduleStore, + api.AGPL.UsageCollector, api.DeploymentValues, provisionerdserver.Options{ ExternalAuthConfigs: api.ExternalAuthConfigs, diff --git a/enterprise/coderd/usage/collector.go b/enterprise/coderd/usage/collector.go index 99dcef6d10217..e25ee76710190 100644 --- a/enterprise/coderd/usage/collector.go +++ b/enterprise/coderd/usage/collector.go @@ -13,17 +13,17 @@ import ( "github.com/coder/quartz" ) -// Collector collects usage events and stores them in the database for +// dbCollector collects usage events and stores them in the database for // publishing. -type Collector struct { +type dbCollector struct { clock quartz.Clock } -var _ agplusage.Collector = &Collector{} +var _ agplusage.Collector = &dbCollector{} -// NewCollector creates a new database-backed usage event collector. -func NewCollector(opts ...CollectorOption) *Collector { - c := &Collector{ +// NewDBCollector creates a new database-backed usage event collector. +func NewDBCollector(opts ...CollectorOption) agplusage.Collector { + c := &dbCollector{ clock: quartz.NewReal(), } for _, opt := range opts { @@ -32,17 +32,17 @@ func NewCollector(opts ...CollectorOption) *Collector { return c } -type CollectorOption func(*Collector) +type CollectorOption func(*dbCollector) // CollectorWithClock sets the quartz clock to use for the collector. func CollectorWithClock(clock quartz.Clock) CollectorOption { - return func(c *Collector) { + return func(c *dbCollector) { c.clock = clock } } // CollectDiscreteUsageEvent implements agplusage.Collector. -func (c *Collector) CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event agplusage.DiscreteEvent) error { +func (c *dbCollector) CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event agplusage.DiscreteEvent) error { if !event.EventType().IsDiscrete() { return xerrors.Errorf("event type %q is not a discrete event", event.EventType()) } diff --git a/enterprise/coderd/usage/collector_test.go b/enterprise/coderd/usage/collector_test.go index 6aa7bc605788b..85c323c864db1 100644 --- a/enterprise/coderd/usage/collector_test.go +++ b/enterprise/coderd/usage/collector_test.go @@ -28,7 +28,7 @@ func TestCollector(t *testing.T) { ctrl := gomock.NewController(t) db := dbmock.NewMockStore(ctrl) clock := quartz.NewMock(t) - collector := usage.NewCollector(usage.CollectorWithClock(clock)) + collector := usage.NewDBCollector(usage.CollectorWithClock(clock)) now := dbtime.Now() events := []struct { @@ -76,7 +76,7 @@ func TestCollector(t *testing.T) { db := dbmock.NewMockStore(ctrl) // We should get an error if the event is invalid. - collector := usage.NewCollector() + collector := usage.NewDBCollector() err := collector.CollectDiscreteUsageEvent(ctx, db, agplusage.DCManagedAgentsV1{ Count: 0, // invalid }) diff --git a/enterprise/coderd/usage/publisher.go b/enterprise/coderd/usage/publisher.go index 290691e44c4ed..57b41cbc9cbb5 100644 --- a/enterprise/coderd/usage/publisher.go +++ b/enterprise/coderd/usage/publisher.go @@ -17,7 +17,6 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/cryptorand" - "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/quartz" ) @@ -47,17 +46,17 @@ type Publisher interface { } type tallymanPublisher struct { - ctx context.Context - ctxCancel context.CancelFunc - log slog.Logger - db database.Store - done chan struct{} + ctx context.Context + ctxCancel context.CancelFunc + log slog.Logger + db database.Store + licenseKeys map[string]ed25519.PublicKey + done chan struct{} // Configured with options: ingestURL string httpClient *http.Client clock quartz.Clock - licenseKeys map[string]ed25519.PublicKey initialDelay time.Duration } @@ -65,19 +64,19 @@ var _ Publisher = &tallymanPublisher{} // NewTallymanPublisher creates a Publisher that publishes usage events to // Coder's Tallyman service. -func NewTallymanPublisher(ctx context.Context, log slog.Logger, db database.Store, opts ...TallymanPublisherOption) Publisher { +func NewTallymanPublisher(ctx context.Context, log slog.Logger, db database.Store, keys map[string]ed25519.PublicKey, opts ...TallymanPublisherOption) Publisher { ctx, cancel := context.WithCancel(ctx) publisher := &tallymanPublisher{ - ctx: ctx, - ctxCancel: cancel, - log: log, - db: db, - done: make(chan struct{}), + ctx: ctx, + ctxCancel: cancel, + log: log, + db: db, + licenseKeys: keys, + done: make(chan struct{}), - ingestURL: tallymanIngestURLV1, - httpClient: http.DefaultClient, - clock: quartz.NewReal(), - licenseKeys: coderd.Keys, + ingestURL: tallymanIngestURLV1, + httpClient: http.DefaultClient, + clock: quartz.NewReal(), } for _, opt := range opts { opt(publisher) @@ -90,6 +89,9 @@ type TallymanPublisherOption func(*tallymanPublisher) // PublisherWithHTTPClient sets the HTTP client to use for publishing usage events. func PublisherWithHTTPClient(httpClient *http.Client) TallymanPublisherOption { return func(p *tallymanPublisher) { + if httpClient == nil { + httpClient = http.DefaultClient + } p.httpClient = httpClient } } @@ -101,14 +103,6 @@ func PublisherWithClock(clock quartz.Clock) TallymanPublisherOption { } } -// PublisherWithLicenseKeys sets the license public keys to use for license -// validation. -func PublisherWithLicenseKeys(keys map[string]ed25519.PublicKey) TallymanPublisherOption { - return func(p *tallymanPublisher) { - p.licenseKeys = keys - } -} - // PublisherWithIngestURL sets the ingest URL to use for publishing usage // events. func PublisherWithIngestURL(ingestURL string) TallymanPublisherOption { @@ -146,6 +140,10 @@ func (p *tallymanPublisher) Start() error { p.initialDelay = tallymanPublishInitialMinimumDelay + time.Duration(plusDelay) } + if len(p.licenseKeys) == 0 { + return xerrors.New("no license keys provided") + } + go p.publishLoop(p.ctx, deploymentUUID) return nil } diff --git a/enterprise/coderd/usage/publisher_test.go b/enterprise/coderd/usage/publisher_test.go index 5e2579a4089ba..96c25a6eb4ee2 100644 --- a/enterprise/coderd/usage/publisher_test.go +++ b/enterprise/coderd/usage/publisher_test.go @@ -61,7 +61,7 @@ func TestIntegration(t *testing.T) { return handler(req) })) - collector := usage.NewCollector( + collector := usage.NewDBCollector( usage.CollectorWithClock(clock), ) // Insert an old event that should never be published. @@ -81,10 +81,9 @@ func TestIntegration(t *testing.T) { require.NoErrorf(t, err, "collecting event %d", i) } - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), ) defer publisher.Close() @@ -213,10 +212,9 @@ func TestPublisherNoEligibleLicenses(t *testing.T) { } })) - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), ) defer publisher.Close() @@ -284,14 +282,13 @@ func TestPublisherClaimExpiry(t *testing.T) { return tallymanAcceptAllHandler(req) })) - collector := usage.NewCollector( + collector := usage.NewDBCollector( usage.CollectorWithClock(clock), ) - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), usage.PublisherWithInitialDelay(17*time.Minute), ) defer publisher.Close() @@ -368,10 +365,9 @@ func TestPublisherMissingEvents(t *testing.T) { } })) - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), ) // Expect the publisher to call SelectUsageEventsForPublishing, followed by @@ -486,10 +482,9 @@ func TestPublisherLicenseSelection(t *testing.T) { return tallymanAcceptAllHandler(req) })) - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), ) defer publisher.Close() @@ -555,10 +550,9 @@ func TestPublisherTallymanError(t *testing.T) { } })) - publisher := usage.NewTallymanPublisher(ctx, log, db, + publisher := usage.NewTallymanPublisher(ctx, log, db, coderdenttest.Keys, usage.PublisherWithClock(clock), usage.PublisherWithIngestURL(ingestURL), - usage.PublisherWithLicenseKeys(coderdenttest.Keys), ) defer publisher.Close()