Skip to content

Commit c9ce4dd

Browse files
easyCZroboquat
authored andcommitted
[usage] Serialize metadata into usage records
1 parent eb9b579 commit c9ce4dd

File tree

3 files changed

+107
-19
lines changed

3 files changed

+107
-19
lines changed

components/usage/pkg/apiv1/usage.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ func (s *UsageService) ReconcileUsageWithLedger(ctx context.Context, req *v1.Rec
341341
logger.Infof("Found %d workspaces instances for usage records in draft.", len(instancesWithUsageInDraft))
342342
instances = append(instances, instancesWithUsageInDraft...)
343343

344-
inserts, updates := reconcileUsageWithLedger(instances, usageDrafts, s.pricer, now)
344+
inserts, updates, err := reconcileUsageWithLedger(instances, usageDrafts, s.pricer, now)
345+
if err != nil {
346+
logger.WithError(err).Errorf("Failed to reconcile usage with ledger.")
347+
return nil, status.Errorf(codes.Internal, "Failed to reconcile usage with ledger.")
348+
}
345349
logger.Infof("Identified %d inserts and %d updates against usage records.", len(inserts), len(updates))
346350

347351
if len(inserts) > 0 {
@@ -365,7 +369,7 @@ func (s *UsageService) ReconcileUsageWithLedger(ctx context.Context, req *v1.Rec
365369
return &v1.ReconcileUsageWithLedgerResponse{}, nil
366370
}
367371

368-
func reconcileUsageWithLedger(instances []db.WorkspaceInstanceForUsage, drafts []db.Usage, pricer *WorkspacePricer, now time.Time) (inserts []db.Usage, updates []db.Usage) {
372+
func reconcileUsageWithLedger(instances []db.WorkspaceInstanceForUsage, drafts []db.Usage, pricer *WorkspacePricer, now time.Time) (inserts []db.Usage, updates []db.Usage, err error) {
369373

370374
instancesByID := dedupeWorkspaceInstancesForUsage(instances)
371375

@@ -376,19 +380,27 @@ func reconcileUsageWithLedger(instances []db.WorkspaceInstanceForUsage, drafts [
376380

377381
for instanceID, instance := range instancesByID {
378382
if usage, exists := draftsByWorkspaceID[instanceID]; exists {
379-
updates = append(updates, updateUsageFromInstance(instance, usage, pricer, now))
383+
updatedUsage, err := updateUsageFromInstance(instance, usage, pricer, now)
384+
if err != nil {
385+
return nil, nil, fmt.Errorf("failed to construct updated usage record: %w", err)
386+
}
387+
updates = append(updates, updatedUsage)
380388
continue
381389
}
382390

383-
inserts = append(inserts, newUsageFromInstance(instance, pricer, now))
391+
usage, err := newUsageFromInstance(instance, pricer, now)
392+
if err != nil {
393+
return nil, nil, fmt.Errorf("failed to construct usage record: %w", err)
394+
}
395+
inserts = append(inserts, usage)
384396
}
385397

386-
return inserts, updates
398+
return inserts, updates, nil
387399
}
388400

389401
const usageDescriptionFromController = "Usage collected by automated system."
390402

391-
func newUsageFromInstance(instance db.WorkspaceInstanceForUsage, pricer *WorkspacePricer, now time.Time) db.Usage {
403+
func newUsageFromInstance(instance db.WorkspaceInstanceForUsage, pricer *WorkspacePricer, now time.Time) (db.Usage, error) {
392404
draft := true
393405
if instance.StoppingTime.IsSet() {
394406
draft = false
@@ -399,7 +411,7 @@ func newUsageFromInstance(instance db.WorkspaceInstanceForUsage, pricer *Workspa
399411
effectiveTime = instance.StoppingTime.Time()
400412
}
401413

402-
return db.Usage{
414+
usage := db.Usage{
403415
ID: uuid.New(),
404416
AttributionID: instance.UsageAttributionID,
405417
Description: usageDescriptionFromController,
@@ -408,17 +420,43 @@ func newUsageFromInstance(instance db.WorkspaceInstanceForUsage, pricer *Workspa
408420
Kind: db.WorkspaceInstanceUsageKind,
409421
WorkspaceInstanceID: instance.ID,
410422
Draft: draft,
411-
Metadata: nil,
412423
}
424+
425+
startedTime := ""
426+
if instance.StartedTime.IsSet() {
427+
startedTime = db.TimeToISO8601(instance.StartedTime.Time())
428+
}
429+
endTime := ""
430+
if instance.StoppingTime.IsSet() {
431+
endTime = db.TimeToISO8601(instance.StoppingTime.Time())
432+
}
433+
err := usage.SetMetadataWithWorkspaceInstance(db.WorkspaceInstanceUsageData{
434+
WorkspaceId: instance.WorkspaceID,
435+
WorkspaceType: instance.Type,
436+
WorkspaceClass: instance.WorkspaceClass,
437+
ContextURL: "",
438+
StartTime: startedTime,
439+
EndTime: endTime,
440+
UserName: "",
441+
UserAvatarURL: "",
442+
})
443+
if err != nil {
444+
return db.Usage{}, fmt.Errorf("failed to serialize workspace instance metadata: %w", err)
445+
}
446+
447+
return usage, nil
413448
}
414449

415-
func updateUsageFromInstance(instance db.WorkspaceInstanceForUsage, usage db.Usage, pricer *WorkspacePricer, now time.Time) db.Usage {
450+
func updateUsageFromInstance(instance db.WorkspaceInstanceForUsage, usage db.Usage, pricer *WorkspacePricer, now time.Time) (db.Usage, error) {
416451
// We construct a new record to ensure we always take the data from the source of truth - the workspace instance
417-
updated := newUsageFromInstance(instance, pricer, now)
452+
updated, err := newUsageFromInstance(instance, pricer, now)
453+
if err != nil {
454+
return db.Usage{}, fmt.Errorf("failed to construct updated usage record: %w", err)
455+
}
418456
// but we override the ID to the one we already have
419457
updated.ID = usage.ID
420458

421-
return updated
459+
return updated, nil
422460
}
423461

424462
func collectWorkspaceInstanceIDs(usage []db.Usage) []uuid.UUID {

components/usage/pkg/apiv1/usage_test.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -624,14 +624,16 @@ func TestReconcileWithLedger(t *testing.T) {
624624
require.NoError(t, err)
625625

626626
t.Run("no action with no instances and no drafts", func(t *testing.T) {
627-
inserts, updates := reconcileUsageWithLedger(nil, nil, pricer, now)
627+
inserts, updates, err := reconcileUsageWithLedger(nil, nil, pricer, now)
628+
require.NoError(t, err)
628629
require.Len(t, inserts, 0)
629630
require.Len(t, updates, 0)
630631
})
631632

632633
t.Run("no action with no instances but existing drafts", func(t *testing.T) {
633634
drafts := []db.Usage{dbtest.NewUsage(t, db.Usage{})}
634-
inserts, updates := reconcileUsageWithLedger(nil, drafts, pricer, now)
635+
inserts, updates, err := reconcileUsageWithLedger(nil, drafts, pricer, now)
636+
require.NoError(t, err)
635637
require.Len(t, inserts, 0)
636638
require.Len(t, updates, 0)
637639
})
@@ -648,12 +650,14 @@ func TestReconcileWithLedger(t *testing.T) {
648650
WorkspaceClass: db.WorkspaceClass_Default,
649651
Type: db.WorkspaceType_Regular,
650652
UsageAttributionID: db.NewTeamAttributionID(uuid.New().String()),
653+
StartedTime: db.NewVarcharTime(now.Add(1 * time.Minute)),
651654
}
652655

653-
inserts, updates := reconcileUsageWithLedger([]db.WorkspaceInstanceForUsage{instance, instance}, nil, pricer, now)
656+
inserts, updates, err := reconcileUsageWithLedger([]db.WorkspaceInstanceForUsage{instance, instance}, nil, pricer, now)
657+
require.NoError(t, err)
654658
require.Len(t, inserts, 1)
655659
require.Len(t, updates, 0)
656-
require.Equal(t, db.Usage{
660+
expectedUsage := db.Usage{
657661
ID: inserts[0].ID,
658662
AttributionID: instance.UsageAttributionID,
659663
Description: usageDescriptionFromController,
@@ -663,7 +667,18 @@ func TestReconcileWithLedger(t *testing.T) {
663667
WorkspaceInstanceID: instance.ID,
664668
Draft: true,
665669
Metadata: nil,
666-
}, inserts[0])
670+
}
671+
require.NoError(t, expectedUsage.SetMetadataWithWorkspaceInstance(db.WorkspaceInstanceUsageData{
672+
WorkspaceId: instance.WorkspaceID,
673+
WorkspaceType: instance.Type,
674+
WorkspaceClass: instance.WorkspaceClass,
675+
ContextURL: "",
676+
StartTime: db.TimeToISO8601(instance.StartedTime.Time()),
677+
EndTime: "",
678+
UserName: "",
679+
UserAvatarURL: "",
680+
}))
681+
require.EqualValues(t, expectedUsage, inserts[0])
667682
})
668683

669684
t.Run("updates a usage record when a draft exists", func(t *testing.T) {
@@ -678,6 +693,7 @@ func TestReconcileWithLedger(t *testing.T) {
678693
WorkspaceClass: db.WorkspaceClass_Default,
679694
Type: db.WorkspaceType_Regular,
680695
UsageAttributionID: db.NewTeamAttributionID(uuid.New().String()),
696+
StartedTime: db.NewVarcharTime(now.Add(1 * time.Minute)),
681697
}
682698

683699
// the fields in the usage record deliberately do not match the instance, except for the Instance ID.
@@ -694,10 +710,12 @@ func TestReconcileWithLedger(t *testing.T) {
694710
Metadata: nil,
695711
})
696712

697-
inserts, updates := reconcileUsageWithLedger([]db.WorkspaceInstanceForUsage{instance}, []db.Usage{draft}, pricer, now)
713+
inserts, updates, err := reconcileUsageWithLedger([]db.WorkspaceInstanceForUsage{instance}, []db.Usage{draft}, pricer, now)
714+
require.NoError(t, err)
698715
require.Len(t, inserts, 0)
699716
require.Len(t, updates, 1)
700-
require.Equal(t, db.Usage{
717+
718+
expectedUsage := db.Usage{
701719
ID: draft.ID,
702720
AttributionID: instance.UsageAttributionID,
703721
Description: usageDescriptionFromController,
@@ -707,6 +725,17 @@ func TestReconcileWithLedger(t *testing.T) {
707725
WorkspaceInstanceID: instance.ID,
708726
Draft: true,
709727
Metadata: nil,
710-
}, updates[0])
728+
}
729+
require.NoError(t, expectedUsage.SetMetadataWithWorkspaceInstance(db.WorkspaceInstanceUsageData{
730+
WorkspaceId: instance.WorkspaceID,
731+
WorkspaceType: instance.Type,
732+
WorkspaceClass: instance.WorkspaceClass,
733+
ContextURL: "",
734+
StartTime: db.TimeToISO8601(instance.StartedTime.Time()),
735+
EndTime: "",
736+
UserName: "",
737+
UserAvatarURL: "",
738+
}))
739+
require.EqualValues(t, expectedUsage, updates[0])
711740
})
712741
}

components/usage/pkg/db/usage.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package db
77
import (
88
"context"
99
"database/sql"
10+
"encoding/json"
1011
"fmt"
1112
"math"
1213
"time"
@@ -47,6 +48,26 @@ type Usage struct {
4748
Metadata datatypes.JSON `gorm:"column:metadata;type:text;size:65535" json:"metadata"`
4849
}
4950

51+
func (u *Usage) SetMetadataWithWorkspaceInstance(data WorkspaceInstanceUsageData) error {
52+
b, err := json.Marshal(data)
53+
if err != nil {
54+
return fmt.Errorf("failed to serialize workspace instance usage data into json: %w", err)
55+
}
56+
57+
u.Metadata = b
58+
return nil
59+
}
60+
61+
func (u *Usage) GetMetadataAsWorkspaceInstanceData() (WorkspaceInstanceUsageData, error) {
62+
var data WorkspaceInstanceUsageData
63+
err := json.Unmarshal(u.Metadata, &data)
64+
if err != nil {
65+
return WorkspaceInstanceUsageData{}, fmt.Errorf("failed unmarshal metadata into wokrspace instance data: %w", err)
66+
}
67+
68+
return data, nil
69+
}
70+
5071
// WorkspaceInstanceUsageData represents the shape of metadata for usage entries of kind "workspaceinstance"
5172
// the equivalent TypeScript definition is maintained in `components/gitpod-protocol/src/usage.ts“
5273
type WorkspaceInstanceUsageData struct {

0 commit comments

Comments
 (0)