Skip to content

chore: move usage types to new package #19103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: 07-30-chore_wire_up_usage_tracking_for_managed_agents
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/hex"
"sort"
"strconv"
"strings"
"time"

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

return m.DebouncedUntil, false
}

func (e UsageEventType) IsDiscrete() bool {
return e.Valid() && strings.HasPrefix(string(e), "dc_")
}

func (e UsageEventType) IsHeartbeat() bool {
return e.Valid() && strings.HasPrefix(string(e), "hb_")
}
15 changes: 6 additions & 9 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ import (
protobuf "google.golang.org/protobuf/proto"

"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"

"github.com/coder/quartz"

"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
Expand All @@ -49,13 +41,18 @@ import (
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/usage"
"github.com/coder/coder/v2/coderd/usage/usagetypes"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/coderd/wspubsub"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/drpcsdk"
"github.com/coder/coder/v2/provisioner"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/quartz"
)

const (
Expand Down Expand Up @@ -1903,7 +1900,7 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
// Collect usage event for managed agents.
usageCollector := s.UsageCollector.Load()
if usageCollector != nil {
event := usage.DCManagedAgentsV1{
event := usagetypes.DCManagedAgentsV1{
Count: 1,
}
err = (*usageCollector).CollectDiscreteUsageEvent(ctx, db, event)
Expand Down
7 changes: 4 additions & 3 deletions coderd/provisionerdserver/provisionerdserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"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/usage/usagetypes"
"github.com/coder/coder/v2/coderd/wspubsub"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
Expand Down Expand Up @@ -2678,7 +2679,7 @@ func TestCompleteJob(t *testing.T) {

// Check that a usage event was collected.
require.Len(t, fakeUsageCollector.collectedEvents, 1)
require.Equal(t, usage.DCManagedAgentsV1{
require.Equal(t, usagetypes.DCManagedAgentsV1{
Count: 1,
}, fakeUsageCollector.collectedEvents[0])
} else {
Expand Down Expand Up @@ -3850,7 +3851,7 @@ func (s *fakeStream) cancel() {
}

type fakeUsageCollector struct {
collectedEvents []usage.Event
collectedEvents []usagetypes.Event
}

var _ usage.Collector = &fakeUsageCollector{}
Expand All @@ -3863,7 +3864,7 @@ func newFakeUsageCollector() (*fakeUsageCollector, *atomic.Pointer[usage.Collect
return fake, ptr
}

func (f *fakeUsageCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, event usage.DiscreteEvent) error {
func (f *fakeUsageCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, event usagetypes.DiscreteEvent) error {
f.collectedEvents = append(f.collectedEvents, event)
return nil
}
5 changes: 3 additions & 2 deletions coderd/usage/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"context"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/usage/usagetypes"
)

// Collector is a sink for usage events generated by the product.
type Collector interface {
// CollectDiscreteUsageEvent writes a discrete usage event to the database
// with the given database or transaction.
CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event DiscreteEvent) error
CollectDiscreteUsageEvent(ctx context.Context, db database.Store, event usagetypes.DiscreteEvent) error
}

// AGPLCollector is a no-op implementation of Collector.
Expand All @@ -24,6 +25,6 @@ func NewAGPLCollector() Collector {

// CollectDiscreteUsageEvent is a no-op implementation of
// CollectDiscreteUsageEvent.
func (AGPLCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, _ DiscreteEvent) error {
func (AGPLCollector) CollectDiscreteUsageEvent(_ context.Context, _ database.Store, _ usagetypes.DiscreteEvent) error {
return nil
}
47 changes: 0 additions & 47 deletions coderd/usage/events.go

This file was deleted.

119 changes: 119 additions & 0 deletions coderd/usage/usagetypes/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package usagetypes contains the types for usage events. These are kept in
// their own package to avoid importing any real code from coderd.
//
// Imports in this package should be limited to the standard library and the
// following packages ONLY:
// - github.com/google/uuid
// - golang.org/x/xerrors
//
// This package is imported by the Tallyman codebase.
package usagetypes

// Please read the package documentation before adding imports.
import (
"bytes"
"encoding/json"
"strings"

"golang.org/x/xerrors"
)

// UsageEventType is an enum of all usage event types. It mirrors the database
// type `usage_event_type`.
type UsageEventType string

const (
UsageEventTypeDCManagedAgentsV1 UsageEventType = "dc_managed_agents_v1"
)

func (e UsageEventType) Valid() bool {
switch e {
case UsageEventTypeDCManagedAgentsV1:
return true
default:
return false
}
}

func (e UsageEventType) IsDiscrete() bool {
return e.Valid() && strings.HasPrefix(string(e), "dc_")
}

func (e UsageEventType) IsHeartbeat() bool {
return e.Valid() && strings.HasPrefix(string(e), "hb_")
}

// ParseEvent parses the raw event data into the specified Go type. It fails if
// there is any unknown fields or extra data after the event. The returned event
// is validated.
func ParseEvent[T Event](data json.RawMessage) (T, error) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()

var event T
err := dec.Decode(&event)
if err != nil {
return event, xerrors.Errorf("unmarshal %T event: %w", event, err)
}
if dec.More() {
return event, xerrors.Errorf("extra data after %T event", event)
}
err = event.Valid()
if err != nil {
return event, xerrors.Errorf("invalid %T event: %w", event, err)
}

return event, nil
}

// ParseEventWithType parses the raw event data into the specified Go type. It
// fails if there is any unknown fields or extra data after the event. The
// returned event is validated.
func ParseEventWithType(eventType UsageEventType, data json.RawMessage) (Event, error) {
switch eventType {
case UsageEventTypeDCManagedAgentsV1:
return ParseEvent[DCManagedAgentsV1](data)
default:
return nil, xerrors.Errorf("unknown event type: %s", eventType)
}
}

// Event is a usage event that can be collected by the usage collector.
//
// Note that the following event types should not be updated once they are
// merged into the product. Please consult Dean before making any changes.
type Event interface {
usageEvent() // to prevent external types from implementing this interface
EventType() UsageEventType
Valid() error
}

// DiscreteEvent is a usage event that is collected as a discrete event.
type DiscreteEvent interface {
Event
discreteUsageEvent() // marker method, also prevents external types from implementing this interface
}

// DCManagedAgentsV1 is a discrete usage event for the number of managed agents.
// This event is sent in the following situations:
// - Once on first startup after usage tracking is added to the product with
// the count of all existing managed agents (count=N)
// - A new managed agent is created (count=1)
type DCManagedAgentsV1 struct {
Count uint64 `json:"count"`
}

var _ DiscreteEvent = DCManagedAgentsV1{}

func (DCManagedAgentsV1) usageEvent() {}
func (DCManagedAgentsV1) discreteUsageEvent() {}
func (DCManagedAgentsV1) EventType() UsageEventType {
return UsageEventTypeDCManagedAgentsV1
}

func (e DCManagedAgentsV1) Valid() error {
if e.Count == 0 {
return xerrors.New("count must be greater than 0")
}
return nil
}
59 changes: 59 additions & 0 deletions coderd/usage/usagetypes/events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package usagetypes_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/usage/usagetypes"
)

func TestParseEvent(t *testing.T) {
t.Parallel()

t.Run("ExtraFields", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1, "extra": "field"}`))
require.ErrorContains(t, err, "unmarshal usagetypes.DCManagedAgentsV1 event")
})

t.Run("ExtraData", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1}{"count": 2}`))
require.ErrorContains(t, err, "extra data after usagetypes.DCManagedAgentsV1 event")
})

t.Run("DCManagedAgentsV1", func(t *testing.T) {
t.Parallel()

event, err := usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": 1}`))
require.NoError(t, err)
require.Equal(t, usagetypes.DCManagedAgentsV1{Count: 1}, event)

_, err = usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{"count": "invalid"}`))
require.ErrorContains(t, err, "unmarshal usagetypes.DCManagedAgentsV1 event")

_, err = usagetypes.ParseEvent[usagetypes.DCManagedAgentsV1]([]byte(`{}`))
require.ErrorContains(t, err, "invalid usagetypes.DCManagedAgentsV1 event: count must be greater than 0")
})
}

func TestParseEventWithType(t *testing.T) {
t.Parallel()

t.Run("UnknownEvent", func(t *testing.T) {
t.Parallel()
_, err := usagetypes.ParseEventWithType(usagetypes.UsageEventType("fake"), []byte(`{}`))
require.ErrorContains(t, err, "unknown event type: fake")
})

t.Run("DCManagedAgentsV1", func(t *testing.T) {
t.Parallel()

eventType := usagetypes.UsageEventTypeDCManagedAgentsV1
event, err := usagetypes.ParseEventWithType(eventType, []byte(`{"count": 1}`))
require.NoError(t, err)
require.Equal(t, usagetypes.DCManagedAgentsV1{Count: 1}, event)
require.Equal(t, eventType, event.EventType())
})
}
Loading
Loading