Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-db/go/usage.go
2497 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package db
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"math"
12
"time"
13
14
"github.com/google/uuid"
15
"gorm.io/datatypes"
16
"gorm.io/gorm"
17
"gorm.io/gorm/clause"
18
)
19
20
type UsageKind string
21
22
const (
23
WorkspaceInstanceUsageKind UsageKind = "workspaceinstance"
24
InvoiceUsageKind UsageKind = "invoice"
25
CreditNoteKind UsageKind = "creditnote"
26
)
27
28
func NewCreditCents(n float64) CreditCents {
29
inCents := n * 100
30
return CreditCents(int64(math.Round(inCents)))
31
}
32
33
type CreditCents int64
34
35
func (cc CreditCents) ToCredits() float64 {
36
return float64(cc) / 100
37
}
38
39
type Usage struct {
40
ID uuid.UUID `gorm:"primary_key;column:id;type:char;size:36;" json:"id"`
41
AttributionID AttributionID `gorm:"column:attributionId;type:varchar;size:255;" json:"attributionId"`
42
Description string `gorm:"column:description;type:varchar;size:255;" json:"description"`
43
CreditCents CreditCents `gorm:"column:creditCents;type:bigint;" json:"creditCents"`
44
EffectiveTime VarcharTime `gorm:"column:effectiveTime;type:varchar;size:255;" json:"effectiveTime"`
45
Kind UsageKind `gorm:"column:kind;type:char;size:10;" json:"kind"`
46
WorkspaceInstanceID *uuid.UUID `gorm:"column:workspaceInstanceId;type:char;size:36;" json:"workspaceInstanceId"`
47
Draft bool `gorm:"column:draft;type:boolean;" json:"draft"`
48
Metadata datatypes.JSON `gorm:"column:metadata;type:text;size:65535" json:"metadata"`
49
}
50
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) SetCreditNoteMetaData(data CreditNoteMetaData) error {
62
b, err := json.Marshal(data)
63
if err != nil {
64
return fmt.Errorf("failed to serialize credit note meta data into json: %w", err)
65
}
66
67
u.Metadata = b
68
return nil
69
}
70
71
func (u *Usage) GetMetadataAsWorkspaceInstanceData() (WorkspaceInstanceUsageData, error) {
72
var data WorkspaceInstanceUsageData
73
err := json.Unmarshal(u.Metadata, &data)
74
if err != nil {
75
return WorkspaceInstanceUsageData{}, fmt.Errorf("failed unmarshal metadata into wokrspace instance data: %w", err)
76
}
77
78
return data, nil
79
}
80
81
// WorkspaceInstanceUsageData represents the shape of metadata for usage entries of kind "workspaceinstance"
82
// the equivalent TypeScript definition is maintained in `components/gitpod-protocol/src/usage.ts“
83
type WorkspaceInstanceUsageData struct {
84
WorkspaceId string `json:"workspaceId"`
85
WorkspaceType WorkspaceType `json:"workspaceType"`
86
WorkspaceClass string `json:"workspaceClass"`
87
ContextURL string `json:"contextURL"`
88
CreationTime string `json:"creationTime"`
89
StartTime string `json:"startTime"`
90
EndTime string `json:"endTime"`
91
StoppedTime string `json:"stoppedTime"`
92
UserID uuid.UUID `json:"userId"`
93
UserName string `json:"userName"`
94
UserAvatarURL string `json:"userAvatarURL"`
95
}
96
97
type CreditNoteMetaData struct {
98
UserID string `json:"userId"`
99
}
100
101
type FindUsageResult struct {
102
UsageEntries []Usage
103
}
104
105
// TableName sets the insert table name for this struct type
106
func (u *Usage) TableName() string {
107
return "d_b_usage"
108
}
109
110
func InsertUsage(ctx context.Context, conn *gorm.DB, records ...Usage) error {
111
return conn.WithContext(ctx).
112
Clauses(clause.OnConflict{DoNothing: true}).
113
CreateInBatches(records, 100).Error
114
}
115
116
func UpdateUsage(ctx context.Context, conn *gorm.DB, records ...Usage) error {
117
for _, record := range records {
118
err := conn.WithContext(ctx).Save(record).Error
119
if err != nil {
120
return fmt.Errorf("failed to update usage record ID: %s: %w", record.ID, err)
121
}
122
}
123
124
return nil
125
}
126
127
func FindAllDraftUsage(ctx context.Context, conn *gorm.DB) ([]Usage, error) {
128
var usageRecords []Usage
129
var usageRecordsBatch []Usage
130
131
result := conn.WithContext(ctx).
132
Where("draft = TRUE").
133
Order("effectiveTime DESC").
134
FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error {
135
usageRecords = append(usageRecords, usageRecordsBatch...)
136
return nil
137
})
138
if result.Error != nil {
139
return nil, fmt.Errorf("failed to get usage records: %s", result.Error)
140
}
141
return usageRecords, nil
142
}
143
144
type FindUsageParams struct {
145
AttributionId AttributionID
146
UserID uuid.UUID
147
From, To time.Time
148
ExcludeDrafts bool
149
Order Order
150
Offset, Limit int64
151
}
152
153
func FindUsage(ctx context.Context, conn *gorm.DB, params *FindUsageParams) ([]Usage, error) {
154
var usageRecords []Usage
155
var usageRecordsBatch []Usage
156
157
db := conn.WithContext(ctx).
158
Where("attributionId = ?", params.AttributionId)
159
if params.UserID != uuid.Nil {
160
db = db.Where("metadata->>'$.userId' = ?", params.UserID.String())
161
}
162
db = db.Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
163
Where("kind = ?", WorkspaceInstanceUsageKind)
164
if params.ExcludeDrafts {
165
db = db.Where("draft = ?", false)
166
}
167
db = db.Order(fmt.Sprintf("effectiveTime %s", params.Order.ToSQL()))
168
if params.Offset != 0 {
169
db = db.Offset(int(params.Offset))
170
}
171
if params.Limit != 0 {
172
db = db.Limit(int(params.Limit))
173
}
174
175
result := db.FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error {
176
usageRecords = append(usageRecords, usageRecordsBatch...)
177
return nil
178
})
179
if result.Error != nil {
180
return nil, fmt.Errorf("failed to get usage records: %s", result.Error)
181
}
182
183
return usageRecords, nil
184
}
185
186
type GetUsageSummaryParams struct {
187
AttributionId AttributionID
188
UserID uuid.UUID
189
From, To time.Time
190
ExcludeDrafts bool
191
}
192
193
type GetUsageSummaryResponse struct {
194
CreditCentsUsed CreditCents
195
NumberOfRecords int
196
}
197
198
func GetUsageSummary(ctx context.Context, conn *gorm.DB, params GetUsageSummaryParams) (GetUsageSummaryResponse, error) {
199
db := conn.WithContext(ctx)
200
query1 := db.Table((&Usage{}).TableName()).
201
Select("sum(creditCents) as CreditCentsUsed, count(*) as NumberOfRecords").
202
Where("attributionId = ?", params.AttributionId)
203
if params.UserID != uuid.Nil {
204
query1 = query1.Where("metadata->>'$.userId' = ?", params.UserID.String())
205
}
206
query1 = query1.Where("effectiveTime >= ? AND effectiveTime < ?", TimeToISO8601(params.From), TimeToISO8601(params.To)).
207
Where("kind = ?", WorkspaceInstanceUsageKind)
208
if params.ExcludeDrafts {
209
query1 = query1.Where("draft = ?", false)
210
}
211
var result GetUsageSummaryResponse
212
err := query1.Find(&result).Error
213
if err != nil {
214
return result, fmt.Errorf("failed to get usage meta data: %w", err)
215
}
216
return result, nil
217
}
218
219
type Balance struct {
220
AttributionID AttributionID `gorm:"column:attributionId;type:varchar;size:255;" json:"attributionId"`
221
CreditCents CreditCents `gorm:"column:creditCents;type:bigint;" json:"creditCents"`
222
}
223
224
func GetBalance(ctx context.Context, conn *gorm.DB, attributionId AttributionID) (CreditCents, error) {
225
rows, err := conn.WithContext(ctx).
226
Model(&Usage{}).
227
Select("sum(creditCents) as balance").
228
Where("attributionId = ?", string(attributionId)).
229
Group("attributionId").
230
Rows()
231
if err != nil {
232
return 0, fmt.Errorf("failed to get rows for list balance query: %w", err)
233
}
234
defer rows.Close()
235
236
if !rows.Next() {
237
return 0, nil
238
}
239
240
var balance CreditCents
241
err = conn.ScanRows(rows, &balance)
242
if err != nil {
243
return 0, fmt.Errorf("failed to scan row: %w", err)
244
}
245
return balance, nil
246
}
247
248
func ListBalance(ctx context.Context, conn *gorm.DB) ([]Balance, error) {
249
var balances []Balance
250
rows, err := conn.WithContext(ctx).
251
Model(&Usage{}).
252
Select("attributionId as attributionId, sum(creditCents) as creditCents").
253
Group("attributionId").
254
Order("attributionId").
255
Rows()
256
if err != nil {
257
return nil, fmt.Errorf("failed to get rows for list balance query: %w", err)
258
}
259
defer rows.Close()
260
261
for rows.Next() {
262
var balance Balance
263
err = conn.ScanRows(rows, &balance)
264
if err != nil {
265
return nil, fmt.Errorf("failed to scan row into Balance struct: %w", err)
266
}
267
balances = append(balances, balance)
268
}
269
270
return balances, nil
271
}
272
273