@@ -45,12 +45,6 @@ type UsageReconcileStatus struct {
45
45
46
46
WorkspaceInstances int
47
47
InvalidWorkspaceInstances int
48
-
49
- Workspaces int
50
-
51
- Teams int
52
-
53
- Report []TeamUsage
54
48
}
55
49
56
50
func (u * UsageReconciler ) Reconcile () error {
@@ -60,7 +54,7 @@ func (u *UsageReconciler) Reconcile() error {
60
54
startOfCurrentMonth := time .Date (now .Year (), now .Month (), 1 , 0 , 0 , 0 , 0 , time .UTC )
61
55
startOfNextMonth := startOfCurrentMonth .AddDate (0 , 1 , 0 )
62
56
63
- status , err := u .ReconcileTimeRange (ctx , startOfCurrentMonth , startOfNextMonth )
57
+ status , report , err := u .ReconcileTimeRange (ctx , startOfCurrentMonth , startOfNextMonth )
64
58
if err != nil {
65
59
return err
66
60
}
@@ -75,7 +69,7 @@ func (u *UsageReconciler) Reconcile() error {
75
69
defer f .Close ()
76
70
77
71
enc := json .NewEncoder (f )
78
- err = enc .Encode (status . Report )
72
+ err = enc .Encode (report )
79
73
if err != nil {
80
74
return fmt .Errorf ("failed to marshal report to JSON: %w" , err )
81
75
}
@@ -89,7 +83,7 @@ func (u *UsageReconciler) Reconcile() error {
89
83
return nil
90
84
}
91
85
92
- func (u * UsageReconciler ) ReconcileTimeRange (ctx context.Context , from , to time.Time ) (* UsageReconcileStatus , error ) {
86
+ func (u * UsageReconciler ) ReconcileTimeRange (ctx context.Context , from , to time.Time ) (* UsageReconcileStatus , UsageReport , error ) {
93
87
now := u .nowFunc ().UTC ()
94
88
log .Infof ("Gathering usage data from %s to %s" , from , to )
95
89
status := & UsageReconcileStatus {
@@ -98,7 +92,7 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
98
92
}
99
93
instances , invalidInstances , err := u .loadWorkspaceInstances (ctx , from , to )
100
94
if err != nil {
101
- return nil , fmt .Errorf ("failed to load workspace instances: %w" , err )
95
+ return nil , nil , fmt .Errorf ("failed to load workspace instances: %w" , err )
102
96
}
103
97
status .WorkspaceInstances = len (instances )
104
98
status .InvalidWorkspaceInstances = len (invalidInstances )
@@ -108,129 +102,38 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
108
102
}
109
103
log .WithField ("workspace_instances" , instances ).Debug ("Successfully loaded workspace instances." )
110
104
111
- workspaces , err := u .loadWorkspaces (ctx , instances )
112
- if err != nil {
113
- return nil , fmt .Errorf ("failed to load workspaces for workspace instances in time range: %w" , err )
114
- }
115
- status .Workspaces = len (workspaces )
116
-
117
- // match workspaces to teams
118
- teams , err := u .loadTeamsForWorkspaces (ctx , workspaces )
119
- if err != nil {
120
- return nil , fmt .Errorf ("failed to load teams for workspaces: %w" , err )
121
- }
122
- status .Teams = len (teams )
123
-
124
- report , err := generateUsageReport (teams , now )
125
- if err != nil {
126
- return nil , fmt .Errorf ("failed to generate usage report: %w" , err )
127
- }
128
- status .Report = report
129
-
130
- u .billingController .Reconcile (status .Report )
131
-
132
- return status , nil
133
- }
134
-
135
- func generateUsageReport (teams []teamWithWorkspaces , maxStopTime time.Time ) ([]TeamUsage , error ) {
136
- var report []TeamUsage
137
- for _ , team := range teams {
138
- var teamTotalRuntime int64
139
- for _ , workspace := range team .Workspaces {
140
- for _ , instance := range workspace .Instances {
141
- teamTotalRuntime += instance .WorkspaceRuntimeSeconds (maxStopTime )
142
- }
143
- }
105
+ instancesByAttributionID := groupInstancesByAttributionID (instances )
144
106
145
- report = append (report , TeamUsage {
146
- TeamID : team .TeamID .String (),
147
- WorkspaceSeconds : teamTotalRuntime ,
148
- })
149
- }
150
- return report , nil
151
- }
107
+ u .billingController .Reconcile (ctx , now , instancesByAttributionID )
152
108
153
- type teamWithWorkspaces struct {
154
- TeamID uuid.UUID
155
- Workspaces []workspaceWithInstances
109
+ return status , instancesByAttributionID , nil
156
110
}
157
111
158
- func (u * UsageReconciler ) loadTeamsForWorkspaces (ctx context.Context , workspaces []workspaceWithInstances ) ([]teamWithWorkspaces , error ) {
159
- // find owner IDs of these workspaces
160
- var ownerIDs []uuid.UUID
161
- for _ , workspace := range workspaces {
162
- ownerIDs = append (ownerIDs , workspace .Workspace .OwnerID )
163
- }
112
+ type UsageReport map [db.AttributionID ][]db.WorkspaceInstance
164
113
165
- // Retrieve memberships. This gives a link between an Owner and a Team they belong to.
166
- memberships , err := db .ListTeamMembershipsForUserIDs (ctx , u .conn , ownerIDs )
167
- if err != nil {
168
- return nil , fmt .Errorf ("failed to list team memberships: %w" , err )
169
- }
114
+ func (u UsageReport ) RuntimeSummaryForTeams (maxStopTime time.Time ) map [string ]int64 {
115
+ attributedUsage := map [string ]int64 {}
170
116
171
- membershipsByUserID := map [uuid. UUID ]db. TeamMembership {}
172
- for _ , membership := range memberships {
173
- // User can belong to multiple teams. For now, we're choosing the membership at random.
174
- membershipsByUserID [ membership . UserID ] = membership
175
- }
117
+ for attribution , instances := range u {
118
+ entity , id := attribution . Values ()
119
+ if entity != "team" {
120
+ continue
121
+ }
176
122
177
- // Convert workspaces into a lookup so that we can index into them by Owner ID, needed for joining Teams with Workspaces
178
- workspacesByOwnerID := map [uuid.UUID ][]workspaceWithInstances {}
179
- for _ , workspace := range workspaces {
180
- workspacesByOwnerID [workspace .Workspace .OwnerID ] = append (workspacesByOwnerID [workspace .Workspace .OwnerID ], workspace )
181
- }
123
+ var runtime uint64
124
+ for _ , instance := range instances {
125
+ runtime += instance .WorkspaceRuntimeSeconds (maxStopTime )
126
+ }
182
127
183
- // Finally, join the datasets
184
- // Because we iterate over memberships, and not workspaces, we're in effect ignoring Workspaces which are not in a team.
185
- // This is intended as we focus on Team usage for now.
186
- var teamsWithWorkspaces []teamWithWorkspaces
187
- for userID , membership := range membershipsByUserID {
188
- teamsWithWorkspaces = append (teamsWithWorkspaces , teamWithWorkspaces {
189
- TeamID : membership .TeamID ,
190
- Workspaces : workspacesByOwnerID [userID ],
191
- })
128
+ attributedUsage [id ] = int64 (runtime )
192
129
}
193
130
194
- return teamsWithWorkspaces , nil
195
- }
196
-
197
- type workspaceWithInstances struct {
198
- Workspace db.Workspace
199
- Instances []db.WorkspaceInstance
131
+ return attributedUsage
200
132
}
201
133
202
- func (u * UsageReconciler ) loadWorkspaces (ctx context.Context , instances []db.WorkspaceInstance ) ([]workspaceWithInstances , error ) {
203
- var workspaceIDs []string
204
- for _ , instance := range instances {
205
- workspaceIDs = append (workspaceIDs , instance .WorkspaceID )
206
- }
207
-
208
- workspaces , err := db .ListWorkspacesByID (ctx , u .conn , toSet (workspaceIDs ))
209
- if err != nil {
210
- return nil , fmt .Errorf ("failed to find workspaces for provided workspace instances: %w" , err )
211
- }
212
-
213
- workspacesByID := map [string ]db.Workspace {}
214
- for _ , workspace := range workspaces {
215
- workspacesByID [workspace .ID ] = workspace
216
- }
217
-
218
- // We need to also add the instances to corresponding records, a single workspace can have multiple instances
219
- instancesByWorkspaceID := map [string ][]db.WorkspaceInstance {}
220
- for _ , instance := range instances {
221
- instancesByWorkspaceID [instance .WorkspaceID ] = append (instancesByWorkspaceID [instance .WorkspaceID ], instance )
222
- }
223
-
224
- // Flatten results into a list
225
- var workspacesWithInstances []workspaceWithInstances
226
- for workspaceID , workspace := range workspacesByID {
227
- workspacesWithInstances = append (workspacesWithInstances , workspaceWithInstances {
228
- Workspace : workspace ,
229
- Instances : instancesByWorkspaceID [workspaceID ],
230
- })
231
- }
232
-
233
- return workspacesWithInstances , nil
134
+ type invalidWorkspaceInstance struct {
135
+ reason string
136
+ workspaceInstanceID uuid.UUID
234
137
}
235
138
236
139
func (u * UsageReconciler ) loadWorkspaceInstances (ctx context.Context , from , to time.Time ) ([]db.WorkspaceInstance , []invalidWorkspaceInstance , error ) {
@@ -246,11 +149,6 @@ func (u *UsageReconciler) loadWorkspaceInstances(ctx context.Context, from, to t
246
149
return trimmed , invalid , nil
247
150
}
248
151
249
- type invalidWorkspaceInstance struct {
250
- reason string
251
- workspaceInstanceID uuid.UUID
252
- }
253
-
254
152
func validateInstances (instances []db.WorkspaceInstance ) (valid []db.WorkspaceInstance , invalid []invalidWorkspaceInstance ) {
255
153
for _ , i := range instances {
256
154
// i is a pointer to the current element, we need to assign it to ensure we're copying the value, not the current pointer.
@@ -302,20 +200,20 @@ func trimStartStopTime(instances []db.WorkspaceInstance, maximumStart, minimumSt
302
200
return updated
303
201
}
304
202
305
- func toSet (items []string ) []string {
306
- m := map [string ]struct {}{}
307
- for _ , i := range items {
308
- m [i ] = struct {}{}
309
- }
310
-
311
- var result []string
312
- for s := range m {
313
- result = append (result , s )
314
- }
315
- return result
316
- }
317
-
318
203
type TeamUsage struct {
319
204
TeamID string `json:"team_id"`
320
205
WorkspaceSeconds int64 `json:"workspace_seconds"`
321
206
}
207
+
208
+ func groupInstancesByAttributionID (instances []db.WorkspaceInstance ) map [db.AttributionID ][]db.WorkspaceInstance {
209
+ result := map [db.AttributionID ][]db.WorkspaceInstance {}
210
+ for _ , instance := range instances {
211
+ if _ , ok := result [instance .UsageAttributionID ]; ! ok {
212
+ result [instance .UsageAttributionID ] = []db.WorkspaceInstance {}
213
+ }
214
+
215
+ result [instance .UsageAttributionID ] = append (result [instance .UsageAttributionID ], instance )
216
+ }
217
+
218
+ return result
219
+ }
0 commit comments