-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Added API call to fetch usage data #12646
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
Conversation
@@ -10,6 +10,9 @@ service UsageService { | |||
// ListBilledUsage retrieves all usage for the specified attributionId | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
components/usage/pkg/apiv1/usage.go
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
components/usage/pkg/db/usage.go
Outdated
if result.Error != nil { | ||
return nil, fmt.Errorf("failed to get usage records: %s", result.Error) | ||
} | ||
return usageRecords, nil | ||
} | ||
|
||
type UsageMetadata struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type UsageMetadata struct { | |
type UsageSummary struct { |
The Metadata keyword gets confusign with the Metadata property on the Usage entry
361e917
to
1fa102e
Compare
components/usage/pkg/apiv1/usage.go
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the type here is rendundant and could be removed
var order db.Order = db.DescendingOrder | |
order := db.DescendingOrder |
components/usage/pkg/apiv1/usage.go
Outdated
} | ||
|
||
usageSummary, err := db.GetUsageSummary(ctx, s.conn, | ||
db.AttributionID(in.GetAttributionId()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should first attempt to parse the attribution using db.ParseAttributionID()
which does a sanity check that the ID is valid.
components/usage/pkg/apiv1/usage.go
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will panic if limit
is 0. We don't validate the pagination input from the request.
components/usage/pkg/apiv1/usage.go
Outdated
var offset int64 = 0 | ||
if in.Pagination != nil { | ||
limit = in.Pagination.PerPage | ||
page = in.Pagination.Page |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For implementing these, you may find gorm scopes very applicable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it only gives me the count not the sum, does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
components/usage/pkg/apiv1/usage.go
Outdated
var limit int64 = 1000 | ||
var page int64 = 0 | ||
var offset int64 = 0 | ||
if in.Pagination != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grpc guarantees it won't be nil. If its not specified in the request, it will contain default values. For ints that's 0
components/usage/pkg/apiv1/usage.go
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AttributionId: db.AttributionID(in.GetAttributionId()), | |
AttributionId: attributionId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, in Go it's a common style to capitalize shorthands. In this case, ID
instead of Id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: