Added API call to fetch usage data#12646
Conversation
| rpc ListBilledUsage(ListBilledUsageRequest) returns (ListBilledUsageResponse) {} | ||
|
|
||
| // ListBilledUsage retrieves all usage for the specified attributionId | ||
| rpc FindUsage(FindUsageRequest) returns (FindUsageResponse) {} |
There was a problem hiding this comment.
If we followed CRUD more closely then the RPC would generally be Get/List/Create/Delete/Update but I'm not too fussed here. It mostly stands out given the RPC above uses List
|
|
||
| message FindUsageResponse { | ||
| repeated BilledSession sessions = 1; | ||
| double total_credits_used = 2; |
There was a problem hiding this comment.
If I understand this correctly then this would return the total credits for the period in the request, correct? For scenarios like this, the proto is a good place to put comments with this as they are included in all of the code-generated clients so docs can be right there with the SDK
fea3159 to
361e917
Compare
| log.Log. | ||
| WithField("attribution_id", in.AttributionId). | ||
| WithField("perPage", limit). | ||
| WithField("page", page). | ||
| WithField("from", from). | ||
| WithField("to", to). | ||
| WithError(err).Error("Failed to fetch usage.") |
There was a problem hiding this comment.
The fields are quite useful even for errors later on. Let's lift this higher like this:
| log.Log. | |
| WithField("attribution_id", in.AttributionId). | |
| WithField("perPage", limit). | |
| WithField("page", page). | |
| WithField("from", from). | |
| WithField("to", to). | |
| WithError(err).Error("Failed to fetch usage.") | |
| logger := log.Log. | |
| WithField("attribution_id", in.AttributionId). | |
| WithField("perPage", limit). | |
| WithField("page", page). | |
| WithField("from", from). | |
| WithField("to", to) | |
| if err != nil { | |
| logger.WithError(err).Error("Failed to fetch usage.") | |
| return ... | |
| } |
This then allows you to re-use the logger fields in other error messages across this method
| return usageRecords, nil | ||
| } | ||
|
|
||
| type UsageMetadata struct { |
There was a problem hiding this comment.
| type UsageMetadata struct { | |
| type UsageSummary struct { |
The Metadata keyword gets confusign with the Metadata property on the Usage entry
361e917 to
1fa102e
Compare
| return nil, status.Errorf(codes.InvalidArgument, "Maximum range exceeded. Range specified can be at most %s", maxQuerySize.String()) | ||
| } | ||
|
|
||
| var order db.Order = db.DescendingOrder |
There was a problem hiding this comment.
I think the type here is rendundant and could be removed
| var order db.Order = db.DescendingOrder | |
| order := db.DescendingOrder |
| } | ||
|
|
||
| usageSummary, err := db.GetUsageSummary(ctx, s.conn, | ||
| db.AttributionID(in.GetAttributionId()), |
There was a problem hiding this comment.
I think we should first attempt to parse the attribution using db.ParseAttributionID() which does a sanity check that the ID is valid.
| logger.WithError(err).Error("Failed to fetch usage metadata.") | ||
| return nil, status.Error(codes.Internal, "unable to retrieve usage") | ||
| } | ||
| var totalPages = int64(math.Ceil(float64(usageSummary.NumRecordsInRange) / float64(limit))) |
There was a problem hiding this comment.
This will panic if limit is 0. We don't validate the pagination input from the request.
| var offset int64 = 0 | ||
| if in.Pagination != nil { | ||
| limit = in.Pagination.PerPage | ||
| page = in.Pagination.Page |
There was a problem hiding this comment.
Here we should also validate the numbers are positive integers
| func FindUsage(ctx context.Context, conn *gorm.DB, params *FindUsageParams) ([]Usage, error) { | ||
| var usageRecords []Usage | ||
| var usageRecordsBatch []Usage | ||
|
|
||
| result := conn.WithContext(ctx). | ||
| Where("attributionId = ?", attributionId). | ||
| Where("? <= effectiveTime AND effectiveTime < ?", from.String(), to.String()). | ||
| Order("effectiveTime DESC"). | ||
| Offset(int(offset)). | ||
| Limit(int(limit)). | ||
| FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error { | ||
| usageRecords = append(usageRecords, usageRecordsBatch...) | ||
| return nil | ||
| }) | ||
| db := conn.WithContext(ctx). | ||
| Where("attributionId = ?", params.AttributionId). | ||
| Where("? <= effectiveTime AND effectiveTime < ?", params.From, params.To) | ||
| if params.ExcludeDrafts { | ||
| db = db.Where("draft = ?", false) | ||
| } | ||
| db = db.Order(fmt.Sprintf("effectiveTime %s", params.Order.ToSQL())) | ||
| if params.Offset != 0 { | ||
| db = db.Offset(int(params.Offset)) | ||
| } | ||
| if params.Limit != 0 { | ||
| db = db.Limit(int(params.Limit)) | ||
| } | ||
|
|
||
| result := db.FindInBatches(&usageRecordsBatch, 1000, func(_ *gorm.DB, _ int) error { | ||
| usageRecords = append(usageRecords, usageRecordsBatch...) | ||
| return nil | ||
| }) | ||
| if result.Error != nil { | ||
| return nil, fmt.Errorf("failed to get usage records: %s", result.Error) | ||
| } | ||
| return usageRecords, nil | ||
| } | ||
|
|
There was a problem hiding this comment.
For implementing these, you may find gorm scopes very applicable
There was a problem hiding this comment.
Not quite sure how you would like them to be used in this context.
| } | ||
| var creditCentsBalanceInPeriod sql.NullInt64 | ||
| var numRecordsInRange sql.NullInt32 | ||
| err = query2.Row().Scan(&creditCentsBalanceInPeriod, &numRecordsInRange) |
There was a problem hiding this comment.
I believe we shouldn't need to use the Row().Scan() API here, and instead should be able to do this with https://gorm.io/docs/advanced_query.html#Count
Happy to try and pair on this, I haven't used it myself yet.
There was a problem hiding this comment.
But it only gives me the count not the sum, does it?
There was a problem hiding this comment.
The intention was to use deserialize into the UsageSummary object directly. If you were to add a gorm annotation to the UsageSummary, you can have those annotations match the returned column names (creditCentsBalanceAtStart, ...)
129ea87 to
99494b5
Compare
| var limit int64 = 1000 | ||
| var page int64 = 0 | ||
| var offset int64 = 0 | ||
| if in.Pagination != nil { |
There was a problem hiding this comment.
Grpc guarantees it won't be nil. If its not specified in the request, it will contain default values. For ints that's 0
| attributionId, err := db.ParseAttributionID(string(usageRecord.AttributionID)) | ||
| if err != nil { | ||
| logger.WithError(err).Errorf("Ignoring usage entry (ID: %s) with invalid attributionID: %s.", usageRecord.ID, usageRecord.AttributionID) | ||
| continue | ||
| } |
There was a problem hiding this comment.
Persoanlly, I'd return an error here from the RPC. Yes, it would cause it to fail but it would more acutely show up and give us a chance to fix. There's a tendency that these logs are missed and never properly fixed.
I think I may have confused you in the previous comment on this. I think when we take the attribution ID from the DB, we can trust it to be the right shape so doing just db.AttributionID(usageRecord.AttributionID) should be fine. But I think we can do better. We can actually type the UsageRecord as an AttributionID directly, this removes this step.
But we should try to parse the Attribution ID from the incoming request. We currently use it on line 208 without parsing (and therefore at least some validation).
There was a problem hiding this comment.
Yes, I got indeed confused (somehow the comments show up on the wrong lines in VS Code sometimes). Makes much more sense now.
99494b5 to
530ed81
Compare
530ed81 to
aff20f9
Compare
| var offset int64 = perPage * page | ||
|
|
||
| listUsageResult, err := db.FindUsage(ctx, s.conn, &db.FindUsageParams{ | ||
| AttributionId: db.AttributionID(in.GetAttributionId()), |
There was a problem hiding this comment.
| AttributionId: db.AttributionID(in.GetAttributionId()), | |
| AttributionId: attributionId, |
There was a problem hiding this comment.
Also, in Go it's a common style to capitalize shorthands. In this case, ID instead of Id
easyCZ
left a comment
There was a problem hiding this comment.
LGTM, left a couple of last comments but these can happen in a follow-up PR
| } | ||
| var creditCentsBalanceInPeriod sql.NullInt64 | ||
| var numRecordsInRange sql.NullInt32 | ||
| err = query2.Row().Scan(&creditCentsBalanceInPeriod, &numRecordsInRange) |
There was a problem hiding this comment.
The intention was to use deserialize into the UsageSummary object directly. If you were to add a gorm annotation to the UsageSummary, you can have those annotations match the returned column names (creditCentsBalanceAtStart, ...)
Description
Added API call to fetch usage data
Related Issue(s)
Fixes #
How to test
Release Notes
Documentation
Werft options: